Merge pull request #15930 from Budibase/csp-whitelist-validation
CSP whitelist validation
This commit is contained in:
commit
95f527613c
|
@ -89,6 +89,8 @@ const CSP_DIRECTIVES = {
|
||||||
"worker-src": ["blob:", "'self'"],
|
"worker-src": ["blob:", "'self'"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CSPDomainRegex = /^[A-Za-z0-9-*:/.]+$/
|
||||||
|
|
||||||
const contentSecurityPolicy = (async (ctx: Ctx, next: Next) => {
|
const contentSecurityPolicy = (async (ctx: Ctx, next: Next) => {
|
||||||
const nonce = crypto.randomBytes(16).toString("base64")
|
const nonce = crypto.randomBytes(16).toString("base64")
|
||||||
ctx.state.nonce = nonce
|
ctx.state.nonce = nonce
|
||||||
|
@ -108,8 +110,8 @@ const contentSecurityPolicy = (async (ctx: Ctx, next: Next) => {
|
||||||
if ("name" in appMetadata) {
|
if ("name" in appMetadata) {
|
||||||
for (let script of appMetadata.scripts || []) {
|
for (let script of appMetadata.scripts || []) {
|
||||||
const inclusions = (script.cspWhitelist || "")
|
const inclusions = (script.cspWhitelist || "")
|
||||||
.split(",")
|
.split("\n")
|
||||||
.filter(url => !!url?.trim().length)
|
.filter(domain => CSPDomainRegex.test(domain))
|
||||||
directives["default-src"] = [
|
directives["default-src"] = [
|
||||||
...directives["default-src"],
|
...directives["default-src"],
|
||||||
...inclusions,
|
...inclusions,
|
||||||
|
|
|
@ -132,4 +132,49 @@ describe("contentSecurityPolicy middleware", () => {
|
||||||
expect(app.getAppMetadata).toHaveBeenCalledWith(appId)
|
expect(app.getAppMetadata).toHaveBeenCalledWith(appId)
|
||||||
expect(next).toHaveBeenCalled()
|
expect(next).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should filter out invalid domains", async () => {
|
||||||
|
const appId = "app_foo"
|
||||||
|
const validDomain = "https://*.foo.bar"
|
||||||
|
const invalidDomain = "https:*&*(£$:\n;"
|
||||||
|
|
||||||
|
// Ctx setup to let us try and use CSP whitelist
|
||||||
|
ctx.appId = appId
|
||||||
|
ctx.user = users.user()
|
||||||
|
ctx.user.license = licenses.license({
|
||||||
|
features: [Feature.CUSTOM_APP_SCRIPTS],
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
app.getAppMetadata.mockImplementation(function (): App {
|
||||||
|
return {
|
||||||
|
appId,
|
||||||
|
type: "foo",
|
||||||
|
version: "1",
|
||||||
|
componentLibraries: [],
|
||||||
|
name: "foo",
|
||||||
|
url: "/foo",
|
||||||
|
template: undefined,
|
||||||
|
instance: { _id: appId },
|
||||||
|
tenantId: ctx.user.tenantId,
|
||||||
|
status: "foo",
|
||||||
|
scripts: [
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
name: "Test",
|
||||||
|
location: "Head",
|
||||||
|
cspWhitelist: validDomain + "\n" + invalidDomain,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await contentSecurityPolicy(ctx, next)
|
||||||
|
|
||||||
|
const cspHeader = ctx.set.mock.calls[0][1]
|
||||||
|
expect(cspHeader).toContain(`default-src 'self' ${validDomain};`)
|
||||||
|
expect(cspHeader).not.toContain(invalidDomain)
|
||||||
|
expect(app.getAppMetadata).toHaveBeenCalledWith(appId)
|
||||||
|
expect(next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue