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
analyze() (src/index.ts:11) passes any string straight through to this function with zero validation in between.
That's fine for the CLI used interactively against a URL a human typed in. It's an SSRF vector the moment this library is embedded in a service that scans user- or customer-supplied targets server-side — which is exactly the stated use case in the README ("ASM platform integrations that need automated header auditing on every deployment") and this repo's own homepage (hailbytes.com/asm).
Concretely, today analyze(untrustedUrl) will happily:
Fetch http://169.254.169.254/latest/meta-data/... (AWS/GCP/Azure instance metadata) and return whatever headers come back.
Fetch http://localhost:6379, http://10.x.x.x, http://192.168.x.x, or any other RFC1918/loopback/link-local address, probing internal services from wherever the scanning service runs.
Follow redirects (redirect: 'follow') from an allowed public host to any of the above, since only the initial URL could plausibly be checked — nothing currently checks any hop.
Fetch DNS names that resolve to internal IPs (DNS rebinding), since there's no post-resolution IP check at all today.
Because the failure mode is "returns an HTTP header report" rather than a crash, this is easy to miss in review — it looks like normal library behavior, not a bug.
Proposed fix
In src/fetch.ts:
Before fetching, reject non-http:/https: schemes with a clear error.
Resolve the hostname (e.g. via dns.lookup) and reject loopback, link-local (including 169.254.0.0/16), private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and unique-local/link-local IPv6 (::1, fc00::/7, fe80::/10) targets by default.
Don't rely on redirect: 'follow' validating only the first hop — either use redirect: 'manual' and re-validate each Location header before following (bounded to a small max hop count), or otherwise ensure every hop is checked.
Add an explicit opt-out, e.g. fetchHeaders(url, { allowPrivateNetworks: true }) / a CLI --allow-private flag, so legitimate local/staging use (http://localhost:3000 during dev) keeps working — this should not be secure-by-accident-breakage, it should be an intentional default with an escape hatch.
It's a security header analysis tool shipping an SSRF gap in its own fetch path — that's a credibility risk if it's ever pointed out publicly, on a project whose whole value proposition is security hardening.
It's a known, well-understood class of bug (OWASP SSRF) with a small, well-scoped fix — no architecture change needed, contained entirely to fetch.ts plus a couple of new tests.
It directly protects the exact deployment model this README advertises (server-side scanning of arbitrary/customer-supplied targets), not just the interactive CLI case.
Problem
fetchHeaders(src/fetch.ts:5) takes any user-supplied URL and fetches it with no scheme, hostname, or IP validation:analyze()(src/index.ts:11) passes any string straight through to this function with zero validation in between.That's fine for the CLI used interactively against a URL a human typed in. It's an SSRF vector the moment this library is embedded in a service that scans user- or customer-supplied targets server-side — which is exactly the stated use case in the README ("ASM platform integrations that need automated header auditing on every deployment") and this repo's own
homepage(hailbytes.com/asm).Concretely, today
analyze(untrustedUrl)will happily:http://169.254.169.254/latest/meta-data/...(AWS/GCP/Azure instance metadata) and return whatever headers come back.http://localhost:6379,http://10.x.x.x,http://192.168.x.x, or any other RFC1918/loopback/link-local address, probing internal services from wherever the scanning service runs.redirect: 'follow') from an allowed public host to any of the above, since only the initial URL could plausibly be checked — nothing currently checks any hop.Because the failure mode is "returns an HTTP header report" rather than a crash, this is easy to miss in review — it looks like normal library behavior, not a bug.
Proposed fix
In
src/fetch.ts:http:/https:schemes with a clear error.dns.lookup) and reject loopback, link-local (including169.254.0.0/16), private (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), and unique-local/link-local IPv6 (::1,fc00::/7,fe80::/10) targets by default.redirect: 'follow'validating only the first hop — either useredirect: 'manual'and re-validate eachLocationheader before following (bounded to a small max hop count), or otherwise ensure every hop is checked.fetchHeaders(url, { allowPrivateNetworks: true })/ a CLI--allow-privateflag, so legitimate local/staging use (http://localhost:3000during dev) keeps working — this should not be secure-by-accident-breakage, it should be an intentional default with an escape hatch.test/fetch.test.ts(this file doesn't exist yet — see Add CLI integration tests —cli.tsandfetch.tshave 0% coverage, including the exit-code CI-gate feature #65) covering: metadata IP rejection, RFC1918 rejection, loopback rejection, non-http(s) scheme rejection, redirect-to-private-IP rejection, and the opt-out flag working.Why this is high-leverage
fetch.tsplus a couple of new tests.