From f62bc9223cf1e0ff57f59747121398fe9e83e2ea Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Mon, 20 Jul 2026 15:10:07 -0400 Subject: [PATCH 1/2] fix(nextjs): allow Clerk protection origins in generated CSP --- .changeset/calm-dragons-protect.md | 5 +++ .../__tests__/content-security-policy.test.ts | 35 ++++++++++++------- .../src/server/content-security-policy.ts | 5 +++ 3 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 .changeset/calm-dragons-protect.md 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..e7f8758bc6b 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,17 @@ describe('CSP Header Utils', () => { expect(cspHeader[1]).toContain(`'nonce-${nonceHeader[1]}'`); }); + it('should allow Clerk abuse and fraud protection origins', () => { + const result = createContentSecurityPolicyHeaders(testHost, {}); + 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'); + } + }); + it('should handle report-only mode', () => { const result = createContentSecurityPolicyHeaders(testHost, { reportOnly: true }); @@ -88,11 +99,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 +248,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 +258,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 +268,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 +314,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 +380,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:'], From 0c37a56f4759d4129f9a50c2d72fcc4ca5a7d753 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Mon, 20 Jul 2026 15:44:59 -0400 Subject: [PATCH 2/2] test(nextjs): cover protection origins in strict CSP --- .../__tests__/content-security-policy.test.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) 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 e7f8758bc6b..67dc14bd692 100644 --- a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts +++ b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts @@ -78,14 +78,25 @@ describe('CSP Header Utils', () => { expect(cspHeader[1]).toContain(`'nonce-${nonceHeader[1]}'`); }); - it('should allow Clerk abuse and fraud protection origins', () => { - const result = createContentSecurityPolicyHeaders(testHost, {}); - 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'); + 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:'); + } } });