diff --git a/.changeset/calm-dragons-protect.md b/.changeset/calm-dragons-protect.md new file mode 100644 index 00000000000..08c7bdca160 --- /dev/null +++ b/.changeset/calm-dragons-protect.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Allow Clerk abuse and fraud protection resources in CSP headers generated by `clerkMiddleware()`. diff --git a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts index b17f34708b8..67dc14bd692 100644 --- a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts +++ b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts @@ -31,11 +31,11 @@ describe('CSP Header Utils', () => { expect(directives).toContainEqual("default-src 'self'"); expect(directives).toContainEqual( - "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com", + "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com", ); expect(directives).toContainEqual("form-action 'self'"); expect(directives).toContainEqual( - "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -78,6 +78,28 @@ describe('CSP Header Utils', () => { expect(cspHeader[1]).toContain(`'nonce-${nonceHeader[1]}'`); }); + it('should allow Clerk abuse and fraud protection origins in default and strict modes', () => { + for (const strict of [false, true]) { + const result = createContentSecurityPolicyHeaders(testHost, { strict }); + const directives = result.headers[0][1].split('; '); + + for (const directiveName of ['script-src', 'connect-src', 'frame-src']) { + const directive = directives.find(d => d.startsWith(directiveName)); + expect(directive).toContain('https://*.protect.clerk.com'); + expect(directive).toContain('https://*.client.protect.clerk.com'); + } + + if (strict) { + const scriptSrcValues = directives + .find(d => d.startsWith('script-src')) + ?.replace('script-src ', '') + .split(' '); + expect(scriptSrcValues).not.toContain('https:'); + expect(scriptSrcValues).not.toContain('http:'); + } + } + }); + it('should handle report-only mode', () => { const result = createContentSecurityPolicyHeaders(testHost, { reportOnly: true }); @@ -88,11 +110,11 @@ describe('CSP Header Utils', () => { const directives = headerValue.split('; '); expect(directives).toContainEqual("default-src 'self'"); expect(directives).toContainEqual( - "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com", + "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com", ); expect(directives).toContainEqual("form-action 'self'"); expect(directives).toContainEqual( - "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -237,7 +259,7 @@ describe('CSP Header Utils', () => { const directives = result.headers[0][1].split('; '); expect(directives).toContainEqual( - `connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com https://api.example.com`, + `connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com https://api.example.com`, ); const imgSrcDirective = directives.find(d => d.startsWith('img-src')) || ''; @@ -247,7 +269,7 @@ describe('CSP Header Utils', () => { expect(imgSrcDirective).toContain('https://images.example.com'); expect(directives).toContainEqual( - `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://frames.example.com`, + `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com https://frames.example.com`, ); }); @@ -257,12 +279,12 @@ describe('CSP Header Utils', () => { const directives = result.headers[0][1].split('; '); expect(directives).toContainEqual( - "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com", + "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com", ); expect(directives).toContainEqual("default-src 'self'"); expect(directives).toContainEqual("form-action 'self'"); expect(directives).toContainEqual( - "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -303,11 +325,11 @@ describe('CSP Header Utils', () => { const directives = result.headers[0][1].split('; '); expect(directives).toContainEqual( - `connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com`, + `connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com`, ); expect(directives).toContainEqual(`img-src 'self' https://img.clerk.com`); expect(directives).toContainEqual( - `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com`, + `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com`, ); expect(directives).toContainEqual(`default-src 'self'`); @@ -369,12 +391,12 @@ describe('CSP Header Utils', () => { const directives = result.headers[0][1].split('; '); expect(directives).toContainEqual( - "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev clerk.example.com", + "connect-src 'self' https://clerk-telemetry.com https://*.clerk-telemetry.com https://api.stripe.com https://maps.googleapis.com https://img.clerk.com https://images.clerkstage.dev https://*.protect.clerk.com https://*.client.protect.clerk.com clerk.example.com", ); expect(directives).toContainEqual("default-src 'self'"); expect(directives).toContainEqual("form-action 'self'"); expect(directives).toContainEqual( - "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com value1 value2", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com https://*.client.protect.clerk.com value1 value2", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); diff --git a/packages/nextjs/src/server/content-security-policy.ts b/packages/nextjs/src/server/content-security-policy.ts index e9c87468c15..145112b0ce5 100644 --- a/packages/nextjs/src/server/content-security-policy.ts +++ b/packages/nextjs/src/server/content-security-policy.ts @@ -1,5 +1,7 @@ import { constants } from '@clerk/backend/internal'; +const clerkProtectionOrigins = ['https://*.protect.clerk.com', 'https://*.client.protect.clerk.com']; + /** * Valid CSP directives according to the CSP Level 3 specification */ @@ -103,6 +105,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://maps.googleapis.com', 'https://img.clerk.com', 'https://images.clerkstage.dev', + ...clerkProtectionOrigins, ], 'default-src': ['self'], 'form-action': ['self'], @@ -112,6 +115,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://*.js.stripe.com', 'https://js.stripe.com', 'https://hooks.stripe.com', + ...clerkProtectionOrigins, ], 'img-src': ['self', 'https://img.clerk.com'], 'script-src': [ @@ -123,6 +127,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://*.js.stripe.com', 'https://js.stripe.com', 'https://maps.googleapis.com', + ...clerkProtectionOrigins, ], 'style-src': ['self', 'unsafe-inline'], 'worker-src': ['self', 'blob:'],