## What
Adds an SSRF guard for server-side image loads.
Satori resolves `<img src>`, SVG `<image href>` and CSS
`background-image` URLs by calling `fetch()` on the server. Without a
guard, an attacker who controls one of those URLs can point it at
internal addresses (`127.0.0.1`, `169.254.169.254` cloud metadata,
RFC-1918 ranges) and — because SVG responses are base64-inlined into the
output — read the body back in-band.
## Changes
- **`src/handler/url-safety.ts`** (new): dependency-free,
runtime-agnostic literal-host classifier. Blocks loopback,
link-local/metadata, RFC-1918, CGNAT, multicast/reserved, and the IPv6
equivalents. Leans on WHATWG `URL` to normalize obfuscated IPv4 forms
(`0x7f000001`, `2130706433`, `127.1`) and decodes IPv4 embedded in IPv6
(mapped `::ffff:`, NAT64 `64:ff9b::`, compatible `::`). Fails closed on
unparseable input and non-`http(s)` protocols.
- **`src/handler/image.ts`**: runs the guard server-only before
fetching. **Fails closed (throws)** — consistent with the existing
absolute-URL validation.
## Scope / known limits
- The guard is **literal-host only**. Hostnames that resolve to private
IPs (DNS rebinding) and HTTP redirects to private addresses are **not**
covered — full coverage needs a connect-time-pinning fetcher (e.g.
`@vercel/safe-fetch`) at the host layer. Documented in the module
header.
- A pluggable `fetcher` option was removed per review (`typeof fetch`
isn't compatible with safe-fetch libraries' redirect policing); it can
be added later with a compatible signature.
## Tests
- `test/url-safety.test.ts`: reported SSRF vectors, obfuscated IPv4,
NAT64/IPv4-compatible IPv6, non-`http(s)` protocols, and public URLs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>