fix(security): extend SSRF protection with private IP range checks#2186
fix(security): extend SSRF protection with private IP range checks#2186tessaherself wants to merge 2 commits intohuggingface:mainfrom
Conversation
The existing isURLLocal only checks 127.0.0.0/8 for IPv4. Add comprehensive private/reserved IP detection covering RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16), and IPv6 equivalents (fc00::/7, fe80::/10). New exports: isPrivateIPv4, isPrivateIPv6, isURLPrivate, validateExternalUrl. All existing exports unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2da2cbaa44
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/lib/server/isURLLocal.ts
Outdated
|
|
||
| /** Check if a URL resolves to a private/reserved IP address (SSRF protection). */ | ||
| export async function isURLPrivate(url: URL): Promise<boolean> { | ||
| const { address, family } = await dnsLookup(url.hostname); |
There was a problem hiding this comment.
Evaluate all DNS records in private-IP check
isURLPrivate only inspects the single address returned by dns.lookup, but hostnames can legitimately return multiple A/AAAA records; in a mixed set (e.g., one public and one private answer), this can return false even though the URL does resolve to a private IP, allowing SSRF checks to be bypassed depending on DNS answer ordering or rebinding timing. Use lookup with all: true and fail if any resolved address is private/reserved.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This feedback doesn't match the actual code. isURLPrivate already uses dnsLookupAll (which calls dns.lookup with { all: true }) and iterates over all resolved addresses, rejecting if any is private. This is exactly what the suggestion recommends — it's already implemented.
Resolve all A/AAAA records with dns.lookup({ all: true }) and reject
if any resolved address is private/reserved, preventing bypass via
mixed DNS answers or rebinding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
isPrivateIPv4(): checks RFC 1918 (10/8, 172.16/12, 192.168/16), loopback, link-local, 0.0.0.0/8isPrivateIPv6(): checks loopback, link-local (fe80::/10), unique-local (fc00::/7)isURLPrivate(): resolves hostname and checks against all private rangesvalidateExternalUrl(): protocol + private IP validation for external fetchesisURLLocal,isURLStringLocal,isHostLocalhost) are unchangedProblem
The current
isURLLocal()only blocks127.0.0.0/8, allowing SSRF to other private ranges like10.x.x.x,172.16.x.x,192.168.x.x, and IPv6 private addresses.Changes
src/lib/server/isURLLocal.ts: Add 4 new exports (purely additive, no breaking changes)Test plan
isURLLocaltests still passisPrivateIPv4("10.0.0.1")returns trueisPrivateIPv4("8.8.8.8")returns falsevalidateExternalUrl("http://192.168.1.1")throws🤖 Generated with Claude Code