Skip to content

Report silently grades the post-redirect URL while labeling it with the pre-redirect one #93

Description

@dmchaledev

Problem

fetchHeaders (src/fetch.ts:14) fetches with redirect: 'follow':

const res = await fetch(url, { method: 'GET', redirect: 'follow', signal: controller.signal });

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:

export async function analyze(input: string | Record<string, string>, options?: FetchOptions): Promise<SecurityHeaderReport> {
  if (typeof input === 'string') {
    const headers = await fetchHeaders(input, options);
    return analyzeHeaders(headers, input);   // <- always labels the report with `input`, never the final URL
  }
  return analyzeHeaders(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.
  • It's a plain correctness/trust bug independent of the SSRF concern in fetchHeaders has no SSRF protection — arbitrary/internal URLs are fetched unchecked #91 — even a fully trusted, entirely public redirect chain triggers it.

Proposed fix

  1. 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.
  2. Add an optional finalUrl?: string field to SecurityHeaderReport (src/types.ts) — additive, non-breaking.
  3. In analyze() (src/index.ts), use fetchHeadersWithMeta and set report.finalUrl whenever it differs from the requested URL.
  4. 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.
  5. Add coverage for this once/alongside the test/fetch.test.ts file proposed in Add CLI integration tests — cli.ts and fetch.ts have 0% coverage, including the exit-code CI-gate feature #65: assert finalUrl is populated and differs from the input URL when the mocked fetch response has a different res.url.

Why this is high-leverage

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions