fix(security): block SSRF in fetchHeaders — reject private/internal targets#92
Open
dmchaledev wants to merge 1 commit into
Open
fix(security): block SSRF in fetchHeaders — reject private/internal targets#92dmchaledev wants to merge 1 commit into
dmchaledev wants to merge 1 commit into
Conversation
…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
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
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.tshttp:/https:schemes with a clear error.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.redirect: 'manual'and re-validate every redirect hop before following it (bounded to 5 hops), instead of only checking the initial URL.fetchHeaders(url, { allowPrivateNetworks: true })and a CLI--allow-privateflag, so local/staging scans (http://localhost:3000) keep working intentionally rather than by accident.src/cli.ts— wires up--allow-privateand updates--helptext.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 theallowPrivateNetworksopt-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.lookupcheck and the actualfetch()call. This PR validates on lookup and on every redirect hop, which closes the straightforward SSRF paths described in #91.Test plan
npm run typecheckpassesnpm test— 97 tests pass (13 new SSRF-focused tests intest/fetch.test.ts)npm run buildsucceeds--allow-privateand--helpCLI output🤖 Generated with Claude Code
https://claude.ai/code/session_01Ezt9SfXUHJVxC9A4baNwou
Generated by Claude Code