From 631545bbedca23924533893324321d7f094042ff Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Fri, 14 Aug 2026 16:54:07 -0400 Subject: [PATCH 1/2] fix(nextjs): allow Clerk protection hosts on all ports in connect-src The generated connect-src listed https://*.protect.clerk.com, and a CSP source with no port matches the scheme's default port only. Requests to those hosts on other ports were therefore blocked by the generated policy. Use a port-inclusive source for connect-src. script-src and frame-src are unchanged, as those are only requested on 443. --- .changeset/dull-donkeys-warn.md | 5 +++++ .../__tests__/content-security-policy.test.ts | 22 ++++++++++++------- .../src/server/content-security-policy.ts | 4 +++- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 .changeset/dull-donkeys-warn.md diff --git a/.changeset/dull-donkeys-warn.md b/.changeset/dull-donkeys-warn.md new file mode 100644 index 00000000000..ee474ec61b2 --- /dev/null +++ b/.changeset/dull-donkeys-warn.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Allow Clerk's abuse and fraud protection hosts on all ports in the generated `connect-src` directive. The `contentSecurityPolicy` option previously emitted `https://*.protect.clerk.com`, which matches port 443 only, so requests to those hosts on other ports were blocked by the resulting policy. 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 5cd460da83f..355bb72dc7e 100644 --- a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts +++ b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts @@ -31,7 +31,7 @@ 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 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( @@ -83,9 +83,15 @@ describe('CSP Header Utils', () => { const result = createContentSecurityPolicyHeaders(testHost, { strict }); const directives = result.headers[0][1].split('; '); - for (const directiveName of ['script-src', 'connect-src', 'frame-src']) { + // connect-src carries the port wildcard because those hosts are also requested on + // ports other than 443, which a portless source would not match. + for (const [directiveName, expectedSource] of [ + ['script-src', 'https://*.protect.clerk.com'], + ['connect-src', 'https://*.protect.clerk.com:*'], + ['frame-src', 'https://*.protect.clerk.com'], + ]) { const directiveSources = directives.find(d => d.startsWith(directiveName))?.split(' '); - expect(directiveSources).toContain('https://*.protect.clerk.com'); + expect(directiveSources).toContain(expectedSource); expect(directiveSources).not.toContain('https://*.clerk.com'); expect(directiveSources).not.toContain('https://*.client.protect.clerk.com'); } @@ -111,7 +117,7 @@ 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 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( @@ -260,7 +266,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 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')) || ''; @@ -280,7 +286,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 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'"); @@ -326,7 +332,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 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( @@ -392,7 +398,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 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'"); diff --git a/packages/nextjs/src/server/content-security-policy.ts b/packages/nextjs/src/server/content-security-policy.ts index ed0599a942d..88b120b2caf 100644 --- a/packages/nextjs/src/server/content-security-policy.ts +++ b/packages/nextjs/src/server/content-security-policy.ts @@ -1,6 +1,8 @@ import { constants } from '@clerk/backend/internal'; const clerkProtectionOrigin = 'https://*.protect.clerk.com'; +// A source with no port matches port 443 only, and these hosts are also requested on other ports. +const clerkProtectionConnectOrigin = `${clerkProtectionOrigin}:*`; /** * Valid CSP directives according to the CSP Level 3 specification @@ -105,7 +107,7 @@ class ContentSecurityPolicyDirectiveManager { 'https://maps.googleapis.com', 'https://img.clerk.com', 'https://images.clerkstage.dev', - clerkProtectionOrigin, + clerkProtectionConnectOrigin, ], 'default-src': ['self'], 'form-action': ['self'], From 2c628d6261cac7ede8e69d5c7a9f2ec298ceb609 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Fri, 14 Aug 2026 17:02:09 -0400 Subject: [PATCH 2/2] test(nextjs): condense the connect-src comment to a single line --- .../src/server/__tests__/content-security-policy.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 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 355bb72dc7e..6952292cf76 100644 --- a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts +++ b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts @@ -83,8 +83,7 @@ describe('CSP Header Utils', () => { const result = createContentSecurityPolicyHeaders(testHost, { strict }); const directives = result.headers[0][1].split('; '); - // connect-src carries the port wildcard because those hosts are also requested on - // ports other than 443, which a portless source would not match. + // connect-src carries the port wildcard: those hosts are also requested on non-443 ports. for (const [directiveName, expectedSource] of [ ['script-src', 'https://*.protect.clerk.com'], ['connect-src', 'https://*.protect.clerk.com:*'],