diff --git a/src/rules.ts b/src/rules.ts index 779cf09..64e5433 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -172,10 +172,22 @@ export function checkXFrameOptions(headers: RawHeaders): HeaderFinding { }; } const val = (raw ?? '').toUpperCase().trim(); - const score = (val === 'DENY' || val === 'SAMEORIGIN') ? 15 : 8; - return { header: 'X-Frame-Options', score, maxScore: 15, status: score === 15 ? 'good' : 'warning', raw, - findings: score < 15 ? [`X-Frame-Options value '${raw}' is not DENY or SAMEORIGIN`] : [], - recommendations: score < 15 ? ['Use DENY or SAMEORIGIN'] : [] }; + if (val === 'DENY' || val === 'SAMEORIGIN') { + return { header: 'X-Frame-Options', score: 15, maxScore: 15, status: 'good', raw, findings: [], recommendations: [] }; + } + // ALLOW-FROM was dropped by every current browser engine (Chrome, Firefox, + // Safari, Edge) years ago — it is silently ignored, not honored, so it + // provides zero real clickjacking protection despite looking like a valid directive. + if (val.startsWith('ALLOW-FROM')) { + return { + header: 'X-Frame-Options', score: 0, maxScore: 15, status: 'warning', raw, + findings: ['ALLOW-FROM is non-standard and ignored by all current browsers — provides no clickjacking protection'], + recommendations: ["Use DENY or SAMEORIGIN, or CSP frame-ancestors to allowlist specific origins"], + }; + } + return { header: 'X-Frame-Options', score: 8, maxScore: 15, status: 'warning', raw, + findings: [`X-Frame-Options value '${raw}' is not DENY or SAMEORIGIN`], + recommendations: ['Use DENY or SAMEORIGIN'] }; } export function checkXContentTypeOptions(headers: RawHeaders): HeaderFinding { diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index ed22923..c04f332 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -288,8 +288,20 @@ describe('checkXFrameOptions', () => { expect(r.score).toBe(15); }); - it('invalid value returns score 8', () => { + it('ALLOW-FROM is ignored by all current browsers and scores 0', () => { const r = checkXFrameOptions({ 'x-frame-options': 'ALLOW-FROM https://example.com' }); + expect(r.score).toBe(0); + expect(r.status).toBe('warning'); + expect(r.findings.some(f => /ignored by all current browsers/i.test(f))).toBe(true); + }); + + it('ALLOW-FROM matching is case-insensitive', () => { + const r = checkXFrameOptions({ 'x-frame-options': 'allow-from https://example.com' }); + expect(r.score).toBe(0); + }); + + it('other invalid value returns score 8', () => { + const r = checkXFrameOptions({ 'x-frame-options': 'garbage-value' }); expect(r.score).toBe(8); expect(r.status).toBe('warning'); });