From cac4b693fb7f4187f3ef6defb2a3906820face5d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 05:10:33 +0000 Subject: [PATCH] fix(csp): flag scheme-only sources (e.g. https:) as permissive in sensitive directives The CSP wildcard check only caught literal `*` but missed scheme-only sources like `https:` or `data:` in sensitive fetch directives. A policy such as `default-src https:` allows loading from any HTTPS host and is nearly as permissive as `*`, yet previously scored as safe. Reuse the existing `isPermissiveSource` helper (which already handles both `*` and scheme-only patterns) in the directive filter, and update the finding message to name both forms. Three new tests cover `https:` in default-src, `data:` in script-src, and the correct non-flagging of `https:` in the low-risk img-src. https://claude.ai/code/session_019zKAbhaab16mcuuP2McH2K --- src/rules.ts | 14 +++++++------- test/analyzer.test.ts | 28 ++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/rules.ts b/src/rules.ts index db5d48c..ff75c5f 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -109,19 +109,19 @@ export function checkCSP(headers: RawHeaders): HeaderFinding { findings.push("'unsafe-eval' allows eval() — potential code injection"); recommendations.push("Remove 'unsafe-eval'"); } - // Check a wildcard (*) source anywhere in the source list of any sensitive - // fetch/navigation directive — not just as the first token of default-src/ - // script-src. img-src/style-src/font-src/media-src are intentionally omitted - // as a wildcard there is low-risk and commonly legitimate. + // Check for overly permissive sources (bare wildcard `*` OR scheme-only like + // `https:` / `data:` which match any host) in sensitive fetch/navigation + // directives. img-src/style-src/font-src/media-src are intentionally omitted + // as a permissive source there is low-risk and commonly legitimate. const wildcardDirectives = ['default-src', 'script-src', 'connect-src', 'form-action', 'frame-src', 'worker-src']; const wildcarded = wildcardDirectives.filter(d => { const sources = extractCspDirective(raw, d); - return sources !== undefined && sources.includes('*'); + return sources !== undefined && sources.some(isPermissiveSource); }); if (wildcarded.length > 0) { score -= 5; - findings.push(`Wildcard (*) source in ${wildcarded.join(', ')} allows any origin`); - recommendations.push('Replace wildcards with specific trusted domains'); + findings.push(`Overly permissive source in ${wildcarded.join(', ')} (e.g. '*' or 'https:') allows any origin`); + recommendations.push('Replace wildcards and scheme-only sources with specific trusted domains'); } // form-action does NOT inherit from default-src, so its absence leaves form // submissions unrestricted even under a strict default-src 'self'. diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index da35ca9..90208c7 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -156,12 +156,28 @@ describe('checkCSP', () => { it('detects wildcard in default-src', () => { const r = checkCSP({ 'content-security-policy': 'default-src *' }); - expect(r.findings.some(f => f.includes('Wildcard'))).toBe(true); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(true); }); it('detects wildcard in script-src', () => { const r = checkCSP({ 'content-security-policy': "default-src 'self'; script-src *" }); - expect(r.findings.some(f => f.includes('Wildcard'))).toBe(true); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(true); + }); + + it('detects scheme-only source (https:) in default-src', () => { + const r = checkCSP({ 'content-security-policy': "default-src https:; form-action 'self'" }); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(true); + expect(r.score).toBeLessThan(20); + }); + + it('detects scheme-only source (data:) in script-src', () => { + const r = checkCSP({ 'content-security-policy': "default-src 'self'; script-src data:; form-action 'self'" }); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(true); + }); + + it('does not flag scheme-only source in low-risk img-src', () => { + const r = checkCSP({ 'content-security-policy': "default-src 'self'; form-action 'self'; img-src https:" }); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(false); }); it("does not penalize 'unsafe-inline' when 'strict-dynamic' + nonce present", () => { @@ -177,24 +193,24 @@ describe('checkCSP', () => { it('detects wildcard in connect-src', () => { const r = checkCSP({ 'content-security-policy': "default-src 'self'; form-action 'self'; connect-src *" }); - expect(r.findings.some(f => /Wildcard.*connect-src/i.test(f))).toBe(true); + expect(r.findings.some(f => /permissive.*connect-src/i.test(f))).toBe(true); expect(r.score).toBe(13); }); it('detects wildcard in form-action', () => { const r = checkCSP({ 'content-security-policy': "default-src 'self'; form-action *" }); - expect(r.findings.some(f => /Wildcard.*form-action/i.test(f))).toBe(true); + expect(r.findings.some(f => /permissive.*form-action/i.test(f))).toBe(true); }); it("detects mid-policy wildcard (default-src 'self' *)", () => { const r = checkCSP({ 'content-security-policy': "default-src 'self' *; form-action 'self'" }); - expect(r.findings.some(f => /Wildcard/i.test(f))).toBe(true); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(true); expect(r.score).toBe(13); }); it('does not flag a wildcard in low-risk img-src', () => { const r = checkCSP({ 'content-security-policy': "default-src 'self'; form-action 'self'; img-src *" }); - expect(r.findings.some(f => /Wildcard/i.test(f))).toBe(false); + expect(r.findings.some(f => /permissive/i.test(f))).toBe(false); expect(r.score).toBe(18); });