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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add ShieldNet-360/secure-vibe --skill ssrf-prevention --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ssrf Prevention?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-ssrf-prevention-secure-vibe)More formats (shields.io, HTML) on the badges page.
---
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 Claude Code. 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`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!