Skip to content

fix: add SSRF protections to fetchHeaders#99

Open
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-m669l9
Open

fix: add SSRF protections to fetchHeaders#99
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-m669l9

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

fetchHeaders (src/fetch.ts) took any user-supplied URL and fetched it with no scheme, hostname, or IP validation, and followed redirects (redirect: 'follow') with no re-validation of where they landed. That's an SSRF vector the moment this library scans customer-/user-supplied targets server-side — exactly the "ASM platform integrations" use case advertised in the README. Concretely, before this change, analyze(untrustedUrl) would happily fetch http://169.254.169.254/latest/meta-data/ (cloud instance metadata), http://127.0.0.1:*, any RFC1918 address, or reach one of those via a redirect from an otherwise-fine public URL.

  • Reject non-http(s) schemes up front.
  • Resolve the hostname (dns/promises.lookup, all addresses) and reject loopback, link-local (169.254.0.0/16 incl. the metadata IP), RFC1918 private ranges, and their IPv6 equivalents (::1, fc00::/7, fe80::/10, IPv4-mapped ::ffff:...) by default.
  • Switch from redirect: 'follow' to redirect: 'manual' and validate every hop the same way before following it, bounded to 5 redirects, instead of only checking the initial URL.
  • Add an explicit opt-out — { allowPrivateNetworks: true } (library) / --allow-private (CLI) — for legitimate local/staging scans, so this is secure-by-default rather than secure-by-accident-breakage.
  • No new runtime dependencies (uses Node's built-in node:dns/promises and node:net), keeping the package's zero-dependency footprint.

Also adds test/fetch.test.ts, which previously had 0% coverage (#65) — 9 cases covering the public-host path, scheme rejection, loopback/RFC1918/metadata-IP rejection, IPv6 loopback/ULA rejection, redirect-to-private rejection, bounded redirect following, the redirect-limit error, and the allowPrivateNetworks opt-out.

Closes #91.

Test plan

  • npm run typecheck passes
  • npm test — 95 tests passing across analyzer.test.ts and the new fetch.test.ts
  • npm run build succeeds
  • npm audit --audit-level=high — 0 vulnerabilities
  • Manually built and ran the CLI:
    • node dist/cli.js http://169.254.169.254/latest/meta-data/ → rejected, exit 1
    • node dist/cli.js http://127.0.0.1:1 → rejected, exit 1
    • node dist/cli.js file:///etc/passwd → rejected (unsupported scheme), exit 1
    • node dist/cli.js https://example.com → normal scan, unaffected (F grade, as expected for a bare site)
    • Started a local HTTP server on 127.0.0.1:4321: blocked by default, and reachable with --allow-private --json, producing a correct report

🤖 Generated with Claude Code

https://claude.ai/code/session_01M9yz9hxzVPHy9QdTvA4tA4


Generated by Claude Code

fetchHeaders took any user-supplied URL and fetched it with no scheme,
hostname, or IP validation, and followed redirects unchecked — an SSRF
vector when this library scans customer-supplied targets server-side
(its advertised ASM/CI-gate use case). It could reach cloud metadata
endpoints, loopback, and RFC1918 addresses, including via a redirect
hop to an otherwise-disallowed target.

Reject non-http(s) schemes, resolve and reject loopback/link-local/
private hostnames by default, and validate each redirect hop instead
of only the initial URL (bounded to 5 hops). Add an explicit opt-out
(allowPrivateNetworks / --allow-private) for legitimate local/staging
scans. Adds test/fetch.test.ts, which previously had 0% coverage (#65).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9yz9hxzVPHy9QdTvA4tA4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetchHeaders has no SSRF protection — arbitrary/internal URLs are fetched unchecked

2 participants