diff --git a/packages/backend-core/src/middleware/contentSecurityPolicy.ts b/packages/backend-core/src/middleware/contentSecurityPolicy.ts index 536b4ef368..c656edf9e6 100644 --- a/packages/backend-core/src/middleware/contentSecurityPolicy.ts +++ b/packages/backend-core/src/middleware/contentSecurityPolicy.ts @@ -89,6 +89,8 @@ const CSP_DIRECTIVES = { "worker-src": ["blob:", "'self'"], } +const CSPDomainRegex = /^[A-Za-z0-9-*:/.]+$/ + const contentSecurityPolicy = (async (ctx: Ctx, next: Next) => { const nonce = crypto.randomBytes(16).toString("base64") ctx.state.nonce = nonce @@ -108,8 +110,8 @@ const contentSecurityPolicy = (async (ctx: Ctx, next: Next) => { if ("name" in appMetadata) { for (let script of appMetadata.scripts || []) { const inclusions = (script.cspWhitelist || "") - .split(",") - .filter(url => !!url?.trim().length) + .split("\n") + .filter(domain => CSPDomainRegex.test(domain)) directives["default-src"] = [ ...directives["default-src"], ...inclusions, diff --git a/packages/backend-core/src/middleware/tests/contentSecurityPolicy.spec.ts b/packages/backend-core/src/middleware/tests/contentSecurityPolicy.spec.ts index 36a313e3f8..d147ecaa65 100644 --- a/packages/backend-core/src/middleware/tests/contentSecurityPolicy.spec.ts +++ b/packages/backend-core/src/middleware/tests/contentSecurityPolicy.spec.ts @@ -132,4 +132,49 @@ describe("contentSecurityPolicy middleware", () => { expect(app.getAppMetadata).toHaveBeenCalledWith(appId) 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() + }) })