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); });