diff --git a/.changeset/free-crews-sin.md b/.changeset/free-crews-sin.md new file mode 100644 index 00000000000..03656083c85 --- /dev/null +++ b/.changeset/free-crews-sin.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': minor +--- + +Adjust the CSP configuration option from mode to boolean strict to make it more intuitive 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 7ab58f52a9f..709492cc15a 100644 --- a/packages/nextjs/src/server/__tests__/content-security-policy.test.ts +++ b/packages/nextjs/src/server/__tests__/content-security-policy.test.ts @@ -21,7 +21,7 @@ describe('CSP Header Utils', () => { const testHost = 'clerk.example.com'; it('should create a standard CSP header with default directives', () => { - const result = createCSPHeader('standard', testHost); + const result = createCSPHeader(false, testHost); const directives = result.header.split('; '); @@ -55,7 +55,7 @@ describe('CSP Header Utils', () => { }); it('should create a strict-dynamic CSP header with nonce', () => { - const result = createCSPHeader('strict-dynamic', testHost); + const result = createCSPHeader(true, testHost); // Extract the script-src directive and verify it contains the required values const directives = result.header.split('; '); @@ -78,7 +78,7 @@ describe('CSP Header Utils', () => { 'img-src': ['self', 'https://example.com'], 'custom-directive': ['value'], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); expect(result.header).toContain("default-src 'none'"); // Check for the presence of all required values in the img-src directive @@ -92,7 +92,7 @@ describe('CSP Header Utils', () => { }); it('should handle development environment specific directives', () => { - const result = createCSPHeader('standard', testHost); + const result = createCSPHeader(false, testHost); const directives = result.header.split('; '); const scriptSrcDirective = directives.find(d => d.startsWith('script-src')); expect(scriptSrcDirective).toBeDefined(); @@ -107,7 +107,7 @@ describe('CSP Header Utils', () => { }); it('preserves all original CLERK_CSP_VALUES directives with special keywords quoted', () => { - const result = createCSPHeader('standard', testHost); + const result = createCSPHeader(false, testHost); // Split the result into individual directives for precise testing const directives = result.header.split('; '); @@ -141,7 +141,7 @@ describe('CSP Header Utils', () => { it('includes script-src with development-specific values when NODE_ENV is not production', () => { vi.stubEnv('NODE_ENV', 'development'); - const result = createCSPHeader('standard', testHost); + const result = createCSPHeader(false, testHost); const directives = result.header.split('; '); const scriptSrc = directives.find((d: string) => d.startsWith('script-src')); @@ -159,7 +159,7 @@ describe('CSP Header Utils', () => { it('properly converts host to clerk subdomain in CSP directives', () => { const host = 'clerk.example.com'; - const result = createCSPHeader('standard', host); + const result = createCSPHeader(false, host); // Split the result into individual directives for precise testing const directives = result.header.split('; '); @@ -184,7 +184,7 @@ describe('CSP Header Utils', () => { const customDirectives = { 'script-src': ["'self'", 'new-value', 'another-value', "'unsafe-inline'", "'unsafe-eval'"], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); // The script-src directive should contain both the default values and new values, with special keywords quoted const resultDirectives = result.header.split('; '); @@ -203,7 +203,7 @@ describe('CSP Header Utils', () => { const customDirectives = { 'object-src': ['self', 'value1', 'value2', 'unsafe-inline'], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); // The new directive should be added const directives = result.header.split('; '); @@ -222,7 +222,7 @@ describe('CSP Header Utils', () => { 'script-src': ['new-value', 'unsafe-inline'], 'object-src': ['self', 'value1', 'value2'], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); // Split the result into individual directives for precise testing const directives = result.header.split('; '); @@ -271,7 +271,7 @@ describe('CSP Header Utils', () => { 'script-src': ['self', 'unsafe-inline', 'unsafe-eval', 'custom-domain.com'], 'new-directive': ['none'], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); // Verify that special keywords are always quoted in output, regardless of input format const resultDirectives = result.header.split('; '); @@ -304,7 +304,7 @@ describe('CSP Header Utils', () => { 'img-src': ['self', 'https://images.example.com'], 'frame-src': ['self', 'https://frames.example.com'], }; - const result = createCSPHeader('standard', testHost, customDirectives); + const result = createCSPHeader(false, testHost, customDirectives); const directives = result.header.split('; '); @@ -330,7 +330,7 @@ describe('CSP Header Utils', () => { }); it('correctly implements strict-dynamic mode with nonce-based script-src', () => { - const result = createCSPHeader('strict-dynamic', testHost); + const result = createCSPHeader(true, testHost); const directives = result.header.split('; '); // Extract the script-src directive and check for specific values diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 6141b0e49d6..9be4ea2c6a0 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -11,7 +11,7 @@ import { withLogger } from '../utils/debugLogger'; import { canUseKeyless } from '../utils/feature-flags'; import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; -import { createCSPHeader, type CSPDirective, type CSPMode } from './content-security-policy'; +import { createCSPHeader, type CSPDirective } from './content-security-policy'; import { errorThrower } from './errorThrower'; import { getKeylessCookieValue } from './keyless'; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from './middleware-storage'; @@ -67,9 +67,10 @@ export type ClerkMiddlewareOptions = AuthenticateRequestOptions & { */ contentSecurityPolicy?: { /** - * The CSP mode to use - either 'standard' or 'strict-dynamic' + * When set to true, enhances security by applying the `strict-dynamic` attribute to the `script-src` CSP directive and generates a unique nonce value to be used for script elements. + * This helps prevent XSS attacks while allowing trusted scripts to execute. */ - mode: CSPMode; + strict: boolean; /** * Custom CSP directives to merge with Clerk's default directives */ @@ -210,7 +211,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl } if (options.contentSecurityPolicy) { const { header, nonce } = createCSPHeader( - options.contentSecurityPolicy.mode, + options.contentSecurityPolicy.strict, (parsePublishableKey(publishableKey)?.frontendApi ?? '').replace('$', ''), options.contentSecurityPolicy.directives, ); diff --git a/packages/nextjs/src/server/content-security-policy.ts b/packages/nextjs/src/server/content-security-policy.ts index 8fb88d03361..0dd123176a6 100644 --- a/packages/nextjs/src/server/content-security-policy.ts +++ b/packages/nextjs/src/server/content-security-policy.ts @@ -39,14 +39,6 @@ export type CSPDirective = | 'style-src-attr' | 'style-src-elem'; -/** - * The mode to use for generating the CSP header - * - * - `standard`: Standard CSP mode - * - `strict-dynamic`: Strict-dynamic mode, also generates a nonce - */ -export type CSPMode = 'standard' | 'strict-dynamic'; - /** * Partial record of directives and their values */ @@ -240,14 +232,14 @@ export function generateNonce(): string { /** * Creates a merged CSP state with all necessary directives - * @param mode - The CSP mode to use + * @param strict - When set to true, enhances security by applying the `strict-dynamic` attribute to the `script-src` CSP directive * @param host - The host to include in CSP * @param customDirectives - Optional custom directives to merge with * @param nonce - Optional nonce for strict-dynamic mode * @returns Merged CSPDirectiveSet */ function createMergedCSP( - mode: CSPMode, + strict: boolean, host: string, customDirectives?: Record, nonce?: string, @@ -257,7 +249,7 @@ function createMergedCSP( mergedCSP['connect-src'].add(host); // Handle strict-dynamic mode specific changes - if (mode === 'strict-dynamic') { + if (strict) { mergedCSP['script-src'].delete('http:'); mergedCSP['script-src'].delete('https:'); mergedCSP['script-src'].add("'strict-dynamic'"); @@ -290,16 +282,16 @@ function createMergedCSP( /** * Creates a Content Security Policy (CSP) header with the specified mode and host - * @param mode - The CSP mode to use ('standard' or 'strict-dynamic') + * @param strict - When set to true, enhances security by applying the `strict-dynamic` attribute to the `script-src` CSP directive * @param host - The host to include in the CSP (parsed from publishableKey) * @param customDirectives - Optional custom directives to merge with * @returns Object containing the formatted CSP header and nonce (if in strict-dynamic mode) */ -export function createCSPHeader(mode: CSPMode, host: string, customDirectives?: CSPValues): CSPHeaderResult { - const nonce = mode === 'strict-dynamic' ? generateNonce() : undefined; +export function createCSPHeader(strict: boolean, host: string, customDirectives?: CSPValues): CSPHeaderResult { + const nonce = strict ? generateNonce() : undefined; return { - header: formatCSPHeader(createMergedCSP(mode, host, customDirectives, nonce)), + header: formatCSPHeader(createMergedCSP(strict, host, customDirectives, nonce)), nonce, }; }