You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It discards res.url (the URL fetch actually landed on after following redirects) and returns only the header map. analyze() (src/index.ts:11-14) then builds the report with url: input — the URL the caller originally passed in, not the one the headers actually came from:
exportasyncfunctionanalyze(input: string|Record<string,string>,options?: FetchOptions): Promise<SecurityHeaderReport>{if(typeofinput==='string'){constheaders=awaitfetchHeaders(input,options);returnanalyzeHeaders(headers,input);// <- always labels the report with `input`, never the final URL}returnanalyzeHeaders(input);}
So whenever the target redirects — http:// → https://, apex → www, a marketing domain → a different app domain/subdomain, a staging host behind a login redirect, etc. — the report says url: "https://example.com" but the grade, score, and every per-header finding actually describe whatever host the redirect chain ended on. There's no field anywhere in SecurityHeaderReport that records the final URL, and no CLI output or JSON field surfaces the discrepancy.
This is easy to miss because the tool still returns a normal-looking report — a grade and findings — it just silently attributes them to the wrong host.
Why this matters here specifically
CI gate use case (the tool's flagship feature):security-headers https://staging.example.com || echo "Gate failed" — if staging.example.com redirects to a login page or a different subdomain with worse headers, the gate fails (or passes) based on a host that isn't the one named in the command, and nothing in the output tells the operator that happened.
Multi-tenant / ASM scanning use case (this repo's stated deployment model): a service scanning many customer-supplied URLs could report host A's grade while having actually graded host B, which matters both for accuracy and for any customer-facing report generated from SecurityHeaderReport.url.
In src/fetch.ts, capture res.url (the fetch spec guarantees this reflects the final URL after redirects) alongside the header map — e.g. add a fetchHeadersWithMeta(url, options) that returns { headers, finalUrl }, with the existing fetchHeaders(url, options) implemented as a thin wrapper around it so the current public signature stays unchanged.
Add an optional finalUrl?: string field to SecurityHeaderReport (src/types.ts) — additive, non-breaking.
In analyze() (src/index.ts), use fetchHeadersWithMeta and set report.finalUrl whenever it differs from the requested URL.
In src/cli.ts's printReport, print a visible note when finalUrl differs from url (e.g. Redirected to: <finalUrl>), and include it as-is in --json output since it's just a report field.
Small, additive, backward-compatible change (no existing exported signatures change) contained to fetch.ts, index.ts, types.ts, and cli.ts.
Fixes a correctness gap in the tool's core promise — "this report describes the security headers of the URL you asked about" — which is foundational for both the CI-gate and ASM-scanning use cases advertised in the README.
Problem
fetchHeaders(src/fetch.ts:14) fetches withredirect: 'follow':It discards
res.url(the URL fetch actually landed on after following redirects) and returns only the header map.analyze()(src/index.ts:11-14) then builds the report withurl: input— the URL the caller originally passed in, not the one the headers actually came from:So whenever the target redirects —
http://→https://, apex →www, a marketing domain → a different app domain/subdomain, a staging host behind a login redirect, etc. — the report saysurl: "https://example.com"but the grade, score, and every per-header finding actually describe whatever host the redirect chain ended on. There's no field anywhere inSecurityHeaderReportthat records the final URL, and no CLI output or JSON field surfaces the discrepancy.This is easy to miss because the tool still returns a normal-looking report — a grade and findings — it just silently attributes them to the wrong host.
Why this matters here specifically
security-headers https://staging.example.com || echo "Gate failed"— ifstaging.example.comredirects to a login page or a different subdomain with worse headers, the gate fails (or passes) based on a host that isn't the one named in the command, and nothing in the output tells the operator that happened.SecurityHeaderReport.url.Proposed fix
src/fetch.ts, captureres.url(the fetch spec guarantees this reflects the final URL after redirects) alongside the header map — e.g. add afetchHeadersWithMeta(url, options)that returns{ headers, finalUrl }, with the existingfetchHeaders(url, options)implemented as a thin wrapper around it so the current public signature stays unchanged.finalUrl?: stringfield toSecurityHeaderReport(src/types.ts) — additive, non-breaking.analyze()(src/index.ts), usefetchHeadersWithMetaand setreport.finalUrlwhenever it differs from the requested URL.src/cli.ts'sprintReport, print a visible note whenfinalUrldiffers fromurl(e.g.Redirected to: <finalUrl>), and include it as-is in--jsonoutput since it's just a report field.test/fetch.test.tsfile proposed in Add CLI integration tests —cli.tsandfetch.tshave 0% coverage, including the exit-code CI-gate feature #65: assertfinalUrlis populated and differs from the input URL when the mockedfetchresponse has a differentres.url.Why this is high-leverage
fetch.ts,index.ts,types.ts, andcli.ts.cli.tsandfetch.tshave 0% coverage, including the exit-code CI-gate feature #65 (missing CLI/fetch test coverage generally) — worth tracking separately since the fix and the "why" are different (accuracy/reporting, not exploitability), even though implementation will likely land in the same area offetch.tsand could reasonably be picked up together with either.