From 0c159519d4c80b533f600a59226a3794d84109bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 05:04:30 +0000 Subject: [PATCH] fix(rules): parse Referrer-Policy as comma-separated list per W3C spec The W3C Referrer Policy spec allows the header value to be a comma-separated list of tokens; browsers use the last recognised value and skip any unrecognised tokens. The previous implementation compared the raw header string directly against the strong-value allowlist, so a valid multi-value header like: Referrer-Policy: no-referrer-when-downgrade, strict-origin-when-cross-origin was incorrectly scored as 5/10 (warning) instead of 10/10 (good), producing false positives in security audits. Fix: split on commas, reverse, and find the last recognised policy token (matching browser behaviour). Adds three new tests covering the multi-value cases. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J7KzZTM9SxvFYPJFbe3qRq --- src/rules.ts | 10 +++++++++- test/analyzer.test.ts | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/rules.ts b/src/rules.ts index 779cf09..ecd4e34 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -202,7 +202,15 @@ export function checkReferrerPolicy(headers: RawHeaders): HeaderFinding { // (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()); + // Per W3C Referrer Policy spec the header value may be a comma-separated list; + // browsers parse left-to-right and use the last token they recognise. Unrecognised + // tokens are skipped, so `unsafe-url, strict-origin-when-cross-origin` is effectively + // strong. We must apply the same last-recognised-wins logic here. + const allValidPolicies = ['no-referrer', 'no-referrer-when-downgrade', 'same-origin', 'origin', + 'strict-origin', 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url']; + const tokens = raw.toLowerCase().split(',').map(t => t.trim()).filter(Boolean); + const effective = [...tokens].reverse().find(t => allValidPolicies.includes(t)) ?? tokens[tokens.length - 1] ?? ''; + const isStrong = strongValues.includes(effective); 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`], diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index ed22923..d3f28b7 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -390,6 +390,27 @@ describe('checkReferrerPolicy', () => { expect(r.score).toBe(5); expect(r.status).toBe('warning'); }); + + it('comma-separated list: uses last recognised value (strong wins)', () => { + // Servers sometimes send a fallback list for older browsers; browsers use the + // last recognised token, so this is effectively strict-origin-when-cross-origin. + const r = checkReferrerPolicy({ 'referrer-policy': 'no-referrer-when-downgrade, strict-origin-when-cross-origin' }); + expect(r.score).toBe(10); + expect(r.status).toBe('good'); + }); + + it('comma-separated list: last recognised weak value is not strong', () => { + const r = checkReferrerPolicy({ 'referrer-policy': 'strict-origin-when-cross-origin, unsafe-url' }); + expect(r.score).toBe(5); + expect(r.status).toBe('warning'); + }); + + it('comma-separated list: unrecognised trailing token falls back to last recognised', () => { + // "future-policy" is not in the spec; browsers ignore it and use strict-origin. + const r = checkReferrerPolicy({ 'referrer-policy': 'strict-origin, future-policy' }); + expect(r.score).toBe(10); + expect(r.status).toBe('good'); + }); }); describe('checkPermissionsPolicy', () => {