Enhance security header analysis with stricter CSP and cross-origin checks#50
Merged
Conversation
The dev dependency @vitest/coverage-v8 was pinned to ^1.6.0 while vitest was on ^4.1.7, producing an ERESOLVE peer-dependency conflict that broke 'npm ci' / 'npm install' without --legacy-peer-deps. Bump coverage-v8 to ^4.1.7 to match the installed vitest major.
Closes #15. The strict Permissions-Policy scoring (requires camera/microphone/geolocation to be restricted for full credit, from #4) left four assertions failing against pre-strict fixtures: - checkPermissionsPolicy feature-policy fallback / precedence / case-insensitive tests used partial policies that now score 5; updated to full strict policies (still exercising the same code paths) and added an explicit test that a partial 'camera=()' policy scores 5/warning. - 'A+ at 90%' grade boundary used a partial policy; updated to the full policy. Also de-flaked 'analyze returns same result as analyzeHeaders': it compared the independently-computed analyzedAt timestamps via toEqual, which could differ by a millisecond. Now compares all other fields and asserts both timestamps are valid ISO strings.
Closes #23. checkXFrameOptions previously awarded a full 15/15 'good' for the mere presence of a frame-ancestors directive in the CSP, so 'frame-ancestors *' or 'frame-ancestors https:' (which permit embedding by any origin and offer zero clickjacking protection) scored identically to 'frame-ancestors none'. Now the directive's source list is parsed: a wildcard (*) or bare-scheme source is treated as permissive and yields status 'warning' (8/15) with a finding, while 'none'/'self'/specific origins remain 'good' (15/15). Adds a reusable extractCspDirective helper used here and by later CSP checks.
… good Closes #22. checkPermissionsPolicy populated the recommendations array unconditionally, so a correctly-configured 'camera=(), microphone=(), geolocation=()' policy returned status 'good' with empty findings but a spurious 'Set Permissions- Policy to...' recommendation — telling developers to set the policy they already had. Every other rule returns recommendations: [] on its good path; this brings checkPermissionsPolicy in line.
Closes #21. Per CSP3 and Google's Strict CSP guidance, 'unsafe-inline' is intentionally included alongside 'strict-dynamic' + a nonce/hash as a backwards-compat fallback; browsers that support 'strict-dynamic' ignore 'unsafe-inline' entirely. checkCSP previously deducted 5 points and emitted a finding for this recommended pattern. It now suppresses the penalty only when both 'strict-dynamic' and a nonce/hash source are present, and still penalizes a bare 'unsafe-inline' (including 'strict-dynamic' without a nonce/hash).
Closes #20. checkCSP only queried the enforcing Content-Security-Policy header, so a report-only deployment (the standard incremental CSP rollout pattern) scored 0/30 'missing' with a 'CSP not present' finding. It now detects Content-Security-Policy-Report-Only when no enforcing header exists and returns partial credit (10/30, 'warning') with feedback to promote the policy to enforcing. An enforcing CSP still takes precedence.
Closes #19. form-action is one of the few CSP directives that does not fall back to default-src, so a policy like 'default-src \'self\'' leaves form submissions entirely unrestricted. checkCSP now deducts 3 points and emits a finding / recommendation when no form-action directive is present. Updated the README recommended policy and scoring table, and the test fixtures intended to represent a fully-hardened CSP, to include form-action 'self'.
…cy values Closes #18. no-referrer-when-downgrade sends the full URL (path and query string) to every cross-origin HTTPS destination — it was the historical browser default specifically because it was the least restrictive option, and Chrome 85 replaced it with strict-origin-when-cross-origin for that reason. It no longer counts as a 'strong' value, so it now scores 5/'warning' instead of 10/'good', matching the README's documented 'strict values only' intent.
Closes #17. checkHSTS awarded the includeSubDomains (+3) and preload (+2) bonuses independently of max-age, so 'max-age=0; includeSubDomains; preload' scored 15/20 'good' — even though max-age=0 is the standard HSTS revocation pattern that purges the host from the browser's HSTS cache and disables HTTPS enforcement. The directive bonuses now only apply when max-age > 0, and max-age=0 emits an explicit revocation finding, yielding 10/20 'warning'.
Closes #16. Wildcard detection previously matched only a '*' appearing as the first token of default-src or script-src, so 'connect-src *', 'form-action *', and mid-policy wildcards like 'default-src \'self\' *' silently passed. It now parses the source list of each sensitive directive (default-src, script-src, connect-src, form-action, frame-src, worker-src) and flags a '*' source anywhere within it. Low-risk directives (img-src, style-src, etc.) are intentionally excluded.
Closes #8. checkCrossOriginPolicies awarded points for the mere presence of COEP/COOP/ CORP headers, so a site explicitly opting out of isolation with 'Cross-Origin-Opener-Policy: unsafe-none', 'Cross-Origin-Embedder-Policy: unsafe-none', and 'Cross-Origin-Resource-Policy: cross-origin' (two of which are browser defaults) still scored 5/5 'good'. Points are now awarded only for restrictive values (COEP require-corp/credentialless, COOP same-origin[-allow- popups], CORP same-origin/same-site); permissive values are flagged with a finding and earn no credit.
Closes #7. Many real-world deployments emit security headers (especially Content-Security-Policy, set by HTML-only middleware, content-type-conditional logic, CDN page rules, or edge workers) only on GET responses, not HEAD. A hard-coded HEAD request therefore systematically reported headers as 'missing' on well-configured sites, producing false D/F grades — and, because the CLI exits non-zero on D/F, breaking CI gates. fetchHeaders now issues GET (as securityheaders.com and Mozilla Observatory do) and discards the response body without reading it so no content is downloaded.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR significantly improves the security header analyzer by implementing stricter Content-Security-Policy (CSP) validation, better cross-origin policy detection, and more nuanced scoring for edge cases. The changes make the analyzer more aligned with modern security best practices.
Key Changes
Content-Security-Policy (CSP) Enhancements
form-actiondirective (or inheritable default-src) to earn full credit, as form-action does not inherit from default-src by default. Missing form-action reduces score by 3 points.X-Frame-Options / CSP frame-ancestors Improvements
frame-ancestorsdirectives with permissive values (wildcard*or bare schemes likehttps:) are now correctly identified as non-protective, returning a warning score of 8.frame-ancestors 'self'with specific trusted origins now correctly scores as protective (15 points).HSTS Enhancements
max-age=0is now correctly identified as a revocation that disables HTTPS enforcement, earning only 10 points with a warning status.Referrer-Policy Refinement
Permissions-Policy Improvements
Cross-Origin Policies Refinement
Fetch Improvements
Testing & Documentation
Implementation Details
extractCspDirective()for robust CSP directive parsingisPermissiveSource()to identify unrestricted source tokenshttps://claude.ai/code/session_01Sq1NTg6S8x66rNXsHfH2cR