fix(cli): re-validate SSRF denylist on redirects + harden isPrivateUrl#1212
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
Review: #1212 — SSRF denylist on redirects + harden isPrivateUrl
Solid hardening. Notes:
isPrivateUrl
- Protocol check moved to top —
file://,ftp://, etc. are now blocked before hostname parsing. ✓ - 0.0.0.0/8 block added (was missing before, maps to localhost on some kernels). ✓
- 127.0.0.0/8 block is now correct (covers all of loopback, not just 127.0.0.1). ✓
- WHATWG URL canonicalization means decimal/hex/octal IPv4 like
http://2130706433/andhttp://0x7f000001/are already normalized to dotted-quad beforeisPrivateIpv4sees them. The tests confirm this assumption. ✓
safeFetch
- Manual redirect loop with
redirect: "manual"re-runsisPrivateUrlon every Location header. This is the correct approach —redirect: "follow"would bypass the check on subsequent hops. ✓ - MAX_FETCH_REDIRECTS=5 is a reasonable cap.
- One potential issue:
new URL(loc, current)will throw iflocis malformed. Wrapping that in try/catch and returning null would be safer, otherwise a redirect to a malformed Location surfaces as an unhandled exception rather than a null return.
fetchBuffer / saveLottieAnimations
- Both callsites switched to
safeFetch. ✓ if (!res || !res.ok)correctly handles the null return. ✓
Minor nit on the redirect resolve: add a try/catch around new URL(loc, current). Otherwise LGTM.
miguel-heygen
left a comment
There was a problem hiding this comment.
One actionable item before merging: in safeFetch, the redirect resolution via new URL(loc, current) can throw if the Location header is malformed. That would surface as an unhandled exception rather than a clean null return. Please wrap it in try/catch and return null on parse failure.
488f5e3 to
74fb9d2
Compare
38d4151 to
a659df1
Compare
|
The try/catch around |
74fb9d2 to
1324de5
Compare
fetchBuffer followed redirects without re-checking the destination, so a public asset URL on a captured page could 30x to an internal or cloud-metadata host (the response then landing on local disk). Add safeFetch, which resolves redirects manually and re-runs the denylist on every hop, and route fetchBuffer and the lottie media fetch through it. Harden isPrivateUrl to also block 0.0.0.0 (and 0.0.0.0/8) and IPv6 loopback, IPv4-mapped, unique-local (fc00::/7) and link-local (fe80::/10) ranges. Alternate IPv4 encodings (decimal/octal/hex) are already normalized to dotted-quad by WHATWG URL parsing, so they were and remain blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
new URL(loc, current) throws a TypeError when the Location header is not a valid URL. Wrap in try/catch and return null so a malformed redirect is treated the same as a blocked/private redirect rather than surfacing as an unhandled exception. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a659df1 to
fd683a1
Compare
The base branch was changed.
Merge activity
|

Summary
safeFetch, a redirect-aware wrapper aroundfetchthat re-runs the SSRF denylist on every hop before following a redirect.fetchBufferand the Lottie media fetch throughsafeFetchso redirect chains can't bounce through a public URL to reach an internal or cloud-metadata host.isPrivateUrlto also block0.0.0.0/0.0.0.0/8, IPv6 loopback (::1), IPv4-mapped (::ffff:…), unique-local (fc00::/7), and link-local (fe80::/10) ranges.Security
F-002 MED —
fetchBufferfollowed redirects without re-checking the denylist on the destination. A30xredirect from an allowlisted public URL to169.254.169.254or an internal host would succeed, leaking the response to the caller (e.g. captured page assets written to local disk).F-003 MED —
isPrivateUrldid not cover0.0.0.0(maps to localhost on most OSes), IPv6 loopback, or IPv6 private ranges. An asset URL using those addresses would bypass the denylist. Alternate IPv4 encodings (decimal/octal/hex) are already normalized to dotted-quad by WHATWG URL parsing and remain blocked.Test plan
isPrivateUrladdress forms (0.0.0.0,::1,fc00::1,fe80::1,::ffff:192.168.1.1)