From 756c8ab433c3b792d7d1c114209fd8b834ed2d15 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 05:05:06 +0000 Subject: [PATCH] fix(referrer-policy): parse comma-separated fallback list per spec The Referrer-Policy header supports a comma-separated list of values; browsers use the last recognised policy in the list. The previous check compared the raw header string against a fixed set of strong values, so a valid header like "unsafe-url, strict-origin-when-cross-origin" was incorrectly graded as a warning even though browsers enforce the strict policy. The fix splits on commas, filters to recognised policy tokens, takes the last one as the effective value, and scores from that. The finding message now reports the effective policy rather than the raw string, which makes remediation guidance actionable. Adds three test cases covering: strong-last list earns full score, weak-last list earns warning, and finding text references effective value. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01Vbd3LGpLmTBEfbH5gKD3Jx --- src/rules.ts | 17 ++++++++++++----- test/analyzer.test.ts | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/rules.ts b/src/rules.ts index 779cf09..496bfe1 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -201,11 +201,18 @@ export function checkReferrerPolicy(headers: RawHeaders): HeaderFinding { // no-referrer-when-downgrade is intentionally excluded: it sends the full URL // (path + query) to every cross-origin HTTPS destination. It was the historical // browser default precisely because it was the least restrictive option. - const strongValues = ['no-referrer', 'strict-origin', 'strict-origin-when-cross-origin', 'same-origin']; - const isStrong = strongValues.includes(raw.toLowerCase().trim()); - const score = isStrong ? 10 : 5; - return { header: 'Referrer-Policy', score, maxScore: 10, status: isStrong ? 'good' : 'warning', raw, - findings: isStrong ? [] : [`Value '${raw}' may leak referrer information`], + const strongValues = new Set(['no-referrer', 'strict-origin', 'strict-origin-when-cross-origin', 'same-origin']); + // Referrer-Policy supports a comma-separated fallback list; browsers use the last recognised value. + // e.g. "unsafe-url, strict-origin-when-cross-origin" is effectively strict-origin-when-cross-origin. + const allValidPolicies = new Set([ + 'no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', + 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url', '', + ]); + const tokens = raw.split(',').map(t => t.trim().toLowerCase()); + const effective = tokens.filter(t => allValidPolicies.has(t)).pop() ?? tokens[tokens.length - 1] ?? raw.toLowerCase().trim(); + const isStrong = strongValues.has(effective); + return { header: 'Referrer-Policy', score: isStrong ? 10 : 5, maxScore: 10, status: isStrong ? 'good' : 'warning', raw, + findings: isStrong ? [] : [`Value '${effective}' may leak referrer information`], recommendations: isStrong ? [] : ['Use: strict-origin-when-cross-origin'] }; } diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index ed22923..30022a8 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -390,6 +390,24 @@ describe('checkReferrerPolicy', () => { expect(r.score).toBe(5); expect(r.status).toBe('warning'); }); + + it('comma-separated fallback list: last recognized strong value earns full score', () => { + const r = checkReferrerPolicy({ 'referrer-policy': 'unsafe-url, strict-origin-when-cross-origin' }); + expect(r.score).toBe(10); + expect(r.status).toBe('good'); + }); + + it('comma-separated fallback list: last recognized weak value gives warning', () => { + const r = checkReferrerPolicy({ 'referrer-policy': 'strict-origin-when-cross-origin, unsafe-url' }); + expect(r.score).toBe(5); + expect(r.status).toBe('warning'); + }); + + it('finding reports the effective value, not the full fallback list string', () => { + const r = checkReferrerPolicy({ 'referrer-policy': 'strict-origin-when-cross-origin, unsafe-url' }); + expect(r.findings[0]).toContain('unsafe-url'); + expect(r.findings[0]).not.toContain('strict-origin-when-cross-origin'); + }); }); describe('checkPermissionsPolicy', () => {