From 0611611e1e7d5838e8b5b61c885c7ef0971c4591 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 05:06:37 +0000 Subject: [PATCH] fix(csp): don't penalize unsafe-inline when nonce/hash present (CSP2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the CSP2 spec, a nonce or hash source anywhere in the policy makes 'unsafe-inline' a no-op in all modern browsers — the token is silently ignored and becomes a backwards-compatibility fallback for CSP1-only clients. The previous logic only suppressed the penalty when BOTH 'strict-dynamic' AND a nonce/hash were present, causing false positives for the common nonce-based CSP2 deployment pattern (no strict-dynamic). Fixes the bypass condition: `!(strict-dynamic && nonce/hash)` → `!nonce/hash`. Removes the now-unused hasStrictDynamic variable and adds two regression tests covering nonce-only and hash-only cases. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01RS9rhhdUe6rUZEEfAgiPxY --- src/rules.ts | 9 ++++----- test/analyzer.test.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/rules.ts b/src/rules.ts index 779cf09..9352c69 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -94,12 +94,11 @@ export function checkCSP(headers: RawHeaders): HeaderFinding { const findings: string[] = []; const recommendations: string[] = []; - // 'unsafe-inline' is ignored by browsers that support 'strict-dynamic' when a - // nonce/hash is also present — that combination is the recommended Strict CSP - // pattern (the 'unsafe-inline' is a backwards-compat fallback), so don't penalize it. - const hasStrictDynamic = /'strict-dynamic'/i.test(raw); + // Per CSP2, a nonce or hash source in the policy causes 'unsafe-inline' to be + // ignored by all modern browsers — it becomes a harmless backwards-compat + // fallback for CSP1-only browsers. Don't penalize it in that case. const hasNonceOrHash = /'nonce-[^']+'/i.test(raw) || /'sha(?:256|384|512)-[^']+'/i.test(raw); - if (/'unsafe-inline'/i.test(raw) && !(hasStrictDynamic && hasNonceOrHash)) { + if (/'unsafe-inline'/i.test(raw) && !hasNonceOrHash) { score -= 5; findings.push("'unsafe-inline' weakens XSS protection"); recommendations.push("Remove 'unsafe-inline'; use nonces or hashes instead"); diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index ed22923..e9f393e 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -170,6 +170,18 @@ describe('checkCSP', () => { expect(r.score).toBe(20); }); + it("does not penalize 'unsafe-inline' when nonce present without 'strict-dynamic' (CSP2 makes it a no-op)", () => { + const r = checkCSP({ 'content-security-policy': "script-src 'nonce-abc123' 'unsafe-inline'; form-action 'self'; base-uri 'none'" }); + expect(r.findings.some(f => f.includes('unsafe-inline'))).toBe(false); + expect(r.score).toBe(20); + }); + + it("does not penalize 'unsafe-inline' when hash present without 'strict-dynamic' (CSP2 makes it a no-op)", () => { + const r = checkCSP({ 'content-security-policy': "script-src 'sha256-abc123def456abc123def456abc123def456abc1' 'unsafe-inline'; form-action 'self'; base-uri 'none'" }); + expect(r.findings.some(f => f.includes('unsafe-inline'))).toBe(false); + expect(r.score).toBe(20); + }); + it("still penalizes 'unsafe-inline' when 'strict-dynamic' present without nonce/hash", () => { const r = checkCSP({ 'content-security-policy': "script-src 'strict-dynamic' 'unsafe-inline'" }); expect(r.findings.some(f => f.includes('unsafe-inline'))).toBe(true);