Skip to content

fix(security): block SSRF in fetchHeaders — reject private/internal targets#92

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

fix(security): block SSRF in fetchHeaders — reject private/internal targets#92
dmchaledev wants to merge 1 commit into
mainfrom
claude/nice-mendel-gmn5pe

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

Fixes #91. fetchHeaders (src/fetch.ts) took any user-supplied URL and fetched it with no scheme, hostname, or IP validation, and followed redirects (redirect: 'follow') without validating any hop past the first. Since this library is designed to be embedded in services that scan user- or customer-supplied targets server-side (the README's stated ASM use case), this was an SSRF vector: analyze(untrustedUrl) would happily fetch cloud metadata endpoints (169.254.169.254), loopback (127.0.0.1), or any RFC1918 address, and a redirect from an allowed public host to any of the above would be followed unchecked.

Changes

  • src/fetch.ts
    • Reject non-http:/https: schemes with a clear error.
    • Resolve hostnames via dns.lookup (literal IPs are checked directly) and reject loopback, link-local (incl. 169.254.0.0/16), RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), CGNAT, and IPv6 loopback/unique-local/link-local (::1, fc00::/7, fe80::/10) targets by default.
    • Switch to redirect: 'manual' and re-validate every redirect hop before following it (bounded to 5 hops), instead of only checking the initial URL.
    • Add an explicit opt-out: fetchHeaders(url, { allowPrivateNetworks: true }) and a CLI --allow-private flag, so local/staging scans (http://localhost:3000) keep working intentionally rather than by accident.
  • src/cli.ts — wires up --allow-private and updates --help text.
  • test/fetch.test.ts (new) — covers scheme rejection, loopback/RFC1918/IPv6/metadata-IP rejection, DNS-resolved private-address rejection, redirect-to-private-IP rejection, redirect-following between public hosts, max-redirect enforcement, and the allowPrivateNetworks opt-out (including that it still blocks disallowed schemes).
  • README.md — documents the new SSRF protection and the opt-out.

Not in scope (noted as a possible future hardening, matching the issue's suggested scope): pinning the DNS-resolved IP at connect time to fully close a TOCTOU/DNS-rebinding window between the dns.lookup check and the actual fetch() call. This PR validates on lookup and on every redirect hop, which closes the straightforward SSRF paths described in #91.

Test plan

  • npm run typecheck passes
  • npm test — 97 tests pass (13 new SSRF-focused tests in test/fetch.test.ts)
  • npm run build succeeds
  • Manually verified --allow-private and --help CLI output

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ezt9SfXUHJVxC9A4baNwou


Generated by Claude Code

…argets

fetchHeaders took any user-supplied URL and fetched it with no scheme,
hostname, or IP validation, and followed redirects blindly. Embedding
this library in a service that scans customer-supplied URLs (the
README's stated ASM use case) let it be used to probe internal
services, cloud metadata endpoints, and loopback/link-local addresses.

Reject non-http(s) schemes, resolve hostnames via dns.lookup and
reject loopback/link-local/RFC1918/RFC4193 targets, and validate every
redirect hop (redirect: 'manual' with a max hop count) instead of only
the initial URL. Add an explicit opt-out (allowPrivateNetworks / CLI
--allow-private) for legitimate local/staging scans.

Fixes #91.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ezt9SfXUHJVxC9A4baNwou
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