diff --git a/src/rules.ts b/src/rules.ts index 779cf09..2e31253 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -63,9 +63,19 @@ export function checkHSTS(headers: RawHeaders): HeaderFinding { // includeSubDomains / preload only add protection when HSTS is actually // enforced; awarding their bonuses under max-age=0 would mask a revocation. if (maxAge > 0) { - if (/includesubdomains/i.test(raw)) { score += 3; } + const hasIncludeSubDomains = /includesubdomains/i.test(raw); + if (hasIncludeSubDomains) { score += 3; } else { findings.push('includeSubDomains not set'); recommendations.push('Add includeSubDomains directive'); } - if (/preload/i.test(raw)) score += 2; + if (/preload/i.test(raw)) { + if (hasIncludeSubDomains) { + score += 2; + } else { + // preload without includeSubDomains is rejected by the HSTS preload list + // (hstspreload.org requires both). The directive is inert in this config. + findings.push('preload requires includeSubDomains — this config is not eligible for the HSTS preload list'); + recommendations.push('Add includeSubDomains alongside preload to qualify for the HSTS preload list'); + } + } } return { header: 'Strict-Transport-Security', score, maxScore: 20, status: score >= 15 ? 'good' : 'warning', raw, findings, recommendations }; diff --git a/test/analyzer.test.ts b/test/analyzer.test.ts index ed22923..ed0441a 100644 --- a/test/analyzer.test.ts +++ b/test/analyzer.test.ts @@ -96,12 +96,19 @@ describe('checkHSTS', () => { expect(r.score).toBe(15); }); - it('preload adds 2 bonus points', () => { + it('preload adds 2 bonus points when includeSubDomains is also set', () => { const withPreload = checkHSTS({ 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload' }); const withoutPreload = checkHSTS({ 'strict-transport-security': 'max-age=31536000; includeSubDomains' }); expect(withPreload.score).toBe(withoutPreload.score + 2); }); + it('preload without includeSubDomains earns no bonus and flags a finding', () => { + const r = checkHSTS({ 'strict-transport-security': 'max-age=31536000; preload' }); + // 10 (base) + 5 (max-age ≥ 1yr) + 0 (no includeSubDomains) + 0 (preload inert) = 15 + expect(r.score).toBe(15); + expect(r.findings.some(f => /preload.*includeSubDomains/i.test(f))).toBe(true); + }); + it('case-insensitive header name matching', () => { const r = checkHSTS({ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload' }); expect(r.score).toBe(20);