From 614bacdd78c757bad8c75688177beb85c33a8244 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Tue, 21 Jul 2026 15:25:27 -0400 Subject: [PATCH 1/2] fix(nextjs): use Clerk wildcard for protection CSP --- .changeset/swift-plums-cover.md | 5 +++ .../__tests__/content-security-policy.test.ts | 33 ++++++++++--------- .../src/server/content-security-policy.ts | 8 ++--- 3 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 .changeset/swift-plums-cover.md diff --git a/.changeset/swift-plums-cover.md b/.changeset/swift-plums-cover.md new file mode 100644 index 00000000000..fa6bc6186f6 --- /dev/null +++ b/.changeset/swift-plums-cover.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Use `https://*.clerk.com` for 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 67dc14bd692..94330dfcd55 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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.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 https://*.protect.clerk.com https://*.client.protect.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -78,15 +78,16 @@ 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', () => { + it('should allow Clerk subdomains needed for abuse and fraud protection 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'); + const directiveSources = directives.find(d => d.startsWith(directiveName))?.split(' '); + expect(directiveSources).toContain('https://*.clerk.com'); + expect(directiveSources).not.toContain('https://*.protect.clerk.com'); + expect(directiveSources).not.toContain('https://*.client.protect.clerk.com'); } if (strict) { @@ -110,11 +111,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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.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 https://*.protect.clerk.com https://*.client.protect.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -259,7 +260,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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.clerk.com clerk.example.com https://api.example.com`, ); const imgSrcDirective = directives.find(d => d.startsWith('img-src')) || ''; @@ -269,7 +270,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://*.protect.clerk.com https://*.client.protect.clerk.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://*.clerk.com https://frames.example.com`, ); }); @@ -279,12 +280,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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.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 https://*.protect.clerk.com https://*.client.protect.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -325,11 +326,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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.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 https://*.protect.clerk.com https://*.client.protect.clerk.com`, + `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.clerk.com`, ); expect(directives).toContainEqual(`default-src 'self'`); @@ -391,12 +392,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 https://*.protect.clerk.com https://*.client.protect.clerk.com 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://*.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 https://*.protect.clerk.com https://*.client.protect.clerk.com value1 value2", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.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 145112b0ce5..a5fa0e60d2c 100644 --- a/packages/nextjs/src/server/content-security-policy.ts +++ b/packages/nextjs/src/server/content-security-policy.ts @@ -1,6 +1,6 @@ import { constants } from '@clerk/backend/internal'; -const clerkProtectionOrigins = ['https://*.protect.clerk.com', 'https://*.client.protect.clerk.com']; +const clerkProtectionOrigin = 'https://*.clerk.com'; /** * Valid CSP directives according to the CSP Level 3 specification @@ -105,7 +105,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://maps.googleapis.com', 'https://img.clerk.com', 'https://images.clerkstage.dev', - ...clerkProtectionOrigins, + clerkProtectionOrigin, ], 'default-src': ['self'], 'form-action': ['self'], @@ -115,7 +115,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://*.js.stripe.com', 'https://js.stripe.com', 'https://hooks.stripe.com', - ...clerkProtectionOrigins, + clerkProtectionOrigin, ], 'img-src': ['self', 'https://img.clerk.com'], 'script-src': [ @@ -127,7 +127,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://*.js.stripe.com', 'https://js.stripe.com', 'https://maps.googleapis.com', - ...clerkProtectionOrigins, + clerkProtectionOrigin, ], 'style-src': ['self', 'unsafe-inline'], 'worker-src': ['self', 'blob:'], From 4eb82e6676e47b48922d4f769c115cab56818230 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Tue, 21 Jul 2026 15:35:19 -0400 Subject: [PATCH 2/2] fix(nextjs): keep protection CSP wildcard scoped --- .changeset/swift-plums-cover.md | 2 +- .../__tests__/content-security-policy.test.ts | 28 +++++++++---------- .../src/server/content-security-policy.ts | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.changeset/swift-plums-cover.md b/.changeset/swift-plums-cover.md index fa6bc6186f6..f1ddcc3f9ce 100644 --- a/.changeset/swift-plums-cover.md +++ b/.changeset/swift-plums-cover.md @@ -2,4 +2,4 @@ '@clerk/nextjs': patch --- -Use `https://*.clerk.com` for Clerk abuse and fraud protection resources in CSP headers generated by `clerkMiddleware()`. +Remove the redundant `https://*.client.protect.clerk.com` source from 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 94330dfcd55..5cd460da83f 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 https://*.clerk.com 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 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 https://*.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -85,8 +85,8 @@ describe('CSP Header Utils', () => { for (const directiveName of ['script-src', 'connect-src', 'frame-src']) { const directiveSources = directives.find(d => d.startsWith(directiveName))?.split(' '); - expect(directiveSources).toContain('https://*.clerk.com'); - expect(directiveSources).not.toContain('https://*.protect.clerk.com'); + expect(directiveSources).toContain('https://*.protect.clerk.com'); + expect(directiveSources).not.toContain('https://*.clerk.com'); expect(directiveSources).not.toContain('https://*.client.protect.clerk.com'); } @@ -111,11 +111,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 https://*.clerk.com 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 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 https://*.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -260,7 +260,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 https://*.clerk.com 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 clerk.example.com https://api.example.com`, ); const imgSrcDirective = directives.find(d => d.startsWith('img-src')) || ''; @@ -270,7 +270,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://*.clerk.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://frames.example.com`, ); }); @@ -280,12 +280,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 https://*.clerk.com 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 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 https://*.clerk.com", + "frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com", ); expect(directives).toContainEqual("img-src 'self' https://img.clerk.com"); expect(directives).toContainEqual("style-src 'self' 'unsafe-inline'"); @@ -326,11 +326,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 https://*.clerk.com 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 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 https://*.clerk.com`, + `frame-src 'self' https://challenges.cloudflare.com https://*.js.stripe.com https://js.stripe.com https://hooks.stripe.com https://*.protect.clerk.com`, ); expect(directives).toContainEqual(`default-src 'self'`); @@ -392,12 +392,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 https://*.clerk.com 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 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 https://*.clerk.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 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 a5fa0e60d2c..ed0599a942d 100644 --- a/packages/nextjs/src/server/content-security-policy.ts +++ b/packages/nextjs/src/server/content-security-policy.ts @@ -1,6 +1,6 @@ import { constants } from '@clerk/backend/internal'; -const clerkProtectionOrigin = 'https://*.clerk.com'; +const clerkProtectionOrigin = 'https://*.protect.clerk.com'; /** * Valid CSP directives according to the CSP Level 3 specification