Server-Side Request Forgery: allowlisting the destination of a server-side fetch, when re-resolution between check and connect matters, redirect and scheme bypasses, parsers that fetch on their own (XXE, SVG, HTML-to-PDF), cloud metadata, and keeping the response from becoming an oracle. Use when fetching a client-supplied URL, wiring webhooks, image proxies, PDF or preview renderers, or reviewing any HTTP-client wrapper.
Installs into .claude/skills of the current project.
Are you the author of Ssrf Prevention?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-ssrf-prevention)
---
name: ssrf-prevention
description: "Server-Side Request Forgery: allowlisting the destination of a server-side fetch, when re-resolution between check and connect matters, redirect and scheme bypasses, parsers that fetch on their own (XXE, SVG, HTML-to-PDF), cloud metadata, and keeping the response from becoming an oracle. Use when fetching a client-supplied URL, wiring webhooks, image proxies, PDF or preview renderers, or reviewing any HTTP-client wrapper."
---
<!-- Native skill bundle for agent-skills (cross-tool convention). Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/ssrf-prevention/SKILL.md. -->
# SSRF Prevention
Server-Side Request Forgery: allowlisting the destination of a server-side fetch, when re-resolution between check and connect matters, redirect and scheme bypasses, parsers that fetch on their own (XXE, SVG, HTML-to-PDF), cloud metadata, and keeping the response from becoming an oracle. Use when fetching a client-supplied URL, wiring webhooks, image proxies, PDF or preview renderers, or reviewing any HTTP-client wrapper.
## ALWAYS
- Constrain the destination with an **allowlist of hosts you intend to reach**, not a denylist of addresses you intend to avoid. A denylist has to enumerate every spelling of every internal address — decimal and octal IPv4, IPv6 and IPv4-mapped IPv6, a hostname whose A record is private, a public wildcard resolver like `nip.io` that encodes any address into a name — and it only has to be wrong once.
- Know which of the two controls you built, because it decides whether re-resolution matters. An **allowlist of hostnames** survives DNS rebinding: the attacker controls DNS only for names they own, and those names are not on the list. An **IP-range check** does not: the name resolves to a public address when you validate and a private one when you connect, and the two resolutions are independent. If your control is address-based, resolve once and **connect to that pinned address** — a custom dialer or transport, carrying the original hostname for SNI and `Host` — so no second lookup can occur.
- Re-validate **at every redirect hop**, or disable redirect-following entirely on fetchers that take user-supplied URLs. An allowed host answering `302` to `http://169.254.169.254/` is the single most common bypass, and it defeats a check performed only on the URL the user submitted.
- Restrict the scheme to `http` and `https` before anything else. `file://`, `gopher://`, `dict://`, `ldap://`, `jar://` and friends turn a fetcher into a file reader or a protocol-smuggling primitive.
- Keep the **user-URL fetcher and the internal fetcher as separate clients**, and make mixing them fail to compile rather than fail at runtime — distinct types in Go, Rust or TypeScript. A single client used for both eventually gets called with the wrong argument.
- Treat a **parser as a fetcher**. XML entity resolution, SVG with external references, HTML-to-PDF renderers, Markdown image embedding, oEmbed and link-preview unfurlers, and office-document converters all issue outbound requests from URLs inside the document, using no code you wrote. `deserialization-security` owns disabling external entities; what belongs here is that the request those parsers make is a server-side fetch and needs the same allowlist and the same egress policy.
- Decide what the **response** may reveal. Returning the body, the status code, the redirect chain, the content type, or the elapsed time to the caller turns a blind SSRF into a readable one. Return a fixed error on failure and never echo the URL or the fetch error back — `error-handling-security` owns the shape of that response.
- Enforce **IMDSv2** on EC2 (session token required, hop limit 1) so a plain `GET` from a compromised process cannot read instance credentials, and give the workload its own identity so the metadata service is not a credential worth reaching. Endpoints for each cloud, and the egress rules that go with them, are in `references/metadata-and-egress.md`.
## NEVER
- Treat "this service is internal / behind a VPN / on an allowlisted network" as mitigation. SSRF originates *inside* that network, so it reaches exactly the services the network control was protecting. Reachability is not authentication.
- Ship a forwarder that switches on the shape of its input — `path.startsWith('http') ? path : base + path`. It is a latent SSRF that activates the day any caller passes user-influenced input.
- Rely on an egress proxy or metadata-blocking sidecar as the **only** control. It constrains where traffic goes, not what the application asks for, and it is bypassed by any request path that does not traverse it.
- Compare a URL as a **string** — prefix match, `contains`, a regex over the raw text. Parse it, then compare the parsed host. `https://allowed.example.com@evil.com/`, `https://evil.com/?x=allowed.example.com` and `https://allowed.example.com.evil.com/` all defeat string matching and none defeat host comparison.
- Accept a hostname without normalizing IDN / Punycode first. Homograph forms compare unequal to the ASCII name they imitate, so a naive allowlist check passes them straight through.
- Parse the URL with one library and fetch it with another. Differential parsing between WHATWG and RFC 3986 implementations is a documented bypass class: the validator and the client disagree about which part is the host.
## KNOWN FALSE POSITIVES
- A URL that is a **configuration constant** — an operator-set integration endpoint, a hard-coded base — is not user input. The static config is the allowlist.
- A forwarder that appends only a path segment or query string to a **literal** base cannot change host, so it is not SSRF. The finding is an absolute URL that user input can influence.
- An outbound webhook **to a customer-supplied URL** is the intended feature of a webhook system; the destination is meant to be arbitrary. It still needs the private and link-local ranges blocked, redirects disabled, and the response withheld from the caller — the delivery result may be reported, never the body.
- A fetch that resolves to a private address is expected when the service's job is to reach an internal host — a monitoring prober, a health checker. The control there is that the *destination set* is operator-defined, not that the address is public.
## Reference files
Read these only when the task calls for them.
- `references/metadata-and-egress.md`
- `references/verifying-findings.md`