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', () => {