Skip to main content

Outbound Request Safety and SSRF

How to fetch user-supplied URLs (webhooks, image imports, link previews, OAuth discovery) without letting a caller reach your internal network or cloud metadata service.

Topic:Security & AuthUpdated:2026-10-14

When Your API Makes the Request

Most API security is about requests coming in. Server-side request forgery (SSRF) is about the requests your API sends out on a caller’s behalf. Any feature that takes a URL from a user and fetches it gives that user a client running inside your network, with your network position, your firewall exemptions and your cloud credentials within reach.

The classic target is the cloud instance metadata service at 169.254.169.254, which on most clouds hands out the machine’s own credentials to anything that can reach it. The OWASP API Security Top 10 (2023) lists this as API7:2023 Server Side Request Forgery, and its second example scenario is precisely a webhook test feature that fetched an attacker’s metadata URL and returned the credentials in the response.

A URL check at input time is not enough on its own, because the hostname that resolves to a public address when you validate it can resolve to a private one when you connect. The defenses below are layered for that reason: each one closes a gap the others leave.


1. Know Where You Fetch URLs

SSRF surfaces are easy to miss because they rarely look like “fetch a URL” in the product spec.

Feature URL supplied as
Webhooks The subscription endpoint, plus any “send test event” button
Image or file import avatar_url, source_url, “import from link”
Link previews and unfurling Any URL inside user content
OAuth and OpenID Connect Issuer metadata at /.well-known/oauth-authorization-server (RFC 8414), and jwks_uri or logo_uri in dynamic client registration (RFC 7591)
Document conversion HTML-to-PDF renderers that follow <img> and <link> tags in the input
Import from another API Feed URLs, OpenAPI import, remote schema $ref resolution
Callbacks for async operations A callback_url on the job request

Inventory these, then route every one of them through a single outbound client that applies the rules below. Five features each doing their own validation is five chances to get it slightly wrong.


2. Allowlist Scheme and Port

Accept https and nothing else. Every other scheme your HTTP library happens to support (file:, gopher:, ftp:, dict:) is attack surface with no product use. Allowing plain http also lets an attacker reach internal services that never bothered with TLS, so require https for webhooks and imports alike; customers who need plain HTTP are rarely the ones you want to make an exception for.

Restrict ports to 443, or to a short published list. An attacker who can choose the port can probe internal services by response timing even when the response body is never returned, which is OWASP’s first API7 scenario.

Parse URLs with one well-maintained, standards-conforming parser, and make the decision on the parsed result, not on the raw string. Parsers disagree on edge cases such as userinfo (https://trusted.example@evil.example/), backslashes and numeric hosts, and an attack that exploits a disagreement needs your validator and your HTTP client to use different parsers. Reject URLs containing userinfo outright; RFC 9110 already forbids senders from generating it in http and https URIs.


3. Block Non-Public Address Ranges

The destination must be a publicly routable address. The authoritative lists are the IANA IPv4 and IPv6 Special-Purpose Address Registries, which mark each block as globally reachable or not. Block every block marked not globally reachable. The ones that matter most:

Block What it is Why it matters
127.0.0.0/8, ::1/128 Loopback Services bound to localhost, admin ports, sidecars
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 Private-use (RFC 1918) Your internal network
169.254.0.0/16 Link-local AWS, Azure and Google Cloud metadata at 169.254.169.254
100.64.0.0/10 Shared address space (carrier-grade NAT) Often used inside cloud and Kubernetes networks
0.0.0.0/8, ::/128 “This network”, unspecified 0.0.0.0 reaches localhost on many systems
fc00::/7 Unique-local (includes all of fd00::/8) AWS metadata over IPv6 at fd00:ec2::254, Google Cloud at fd20:ce::254
fe80::/10 IPv6 link-local The IPv6 equivalent of 169.254.0.0/16
::ffff:0:0/96 IPv4-mapped IPv6 ::ffff:169.254.169.254 is the metadata service written in IPv6
192.0.0.0/24, 198.18.0.0/15, 240.0.0.0/4, 255.255.255.255/32 Protocol assignments, benchmarking, reserved, broadcast Not legitimate webhook destinations

Three traps:

  • Check the address, not the string. 2130706433, 0x7f.1 and 017700000001 are all ways of writing 127.0.0.1 that URL parsers accept. Resolve to a binary address first, then compare it against CIDR ranges.
  • Unwrap embedded IPv4. IPv4-mapped addresses and NAT64 prefixes such as 64:ff9b::/96 carry an IPv4 address inside an IPv6 one. Extract it and check it against the IPv4 rules too.
  • Do not hand-maintain the list from memory. Use a library that tracks the IANA registries, and re-check when the registries change; the IPv6 registry gained new non-reachable blocks in recent years.

Blocking ranges is necessary and not sufficient. Your own infrastructure may have internal services on public addresses, and none of this stops the rebinding attack in the next section.


4. Resolve Once, Then Connect to That Address

The naive implementation resolves the hostname, checks the address, then hands the URL to an HTTP client, which resolves it again. Between the two lookups, an attacker who controls the domain’s DNS can change the answer: a public address with a short TTL for the check, 169.254.169.254 for the connection. This is DNS rebinding, and it defeats every validator that checks a hostname rather than the socket.

The defense is to make the check and the connection use the same resolution:

addresses = resolve(host) # all A and AAAA records
if any address is not globally reachable: # reject if ANY fails, not just the first
reject
connect to addresses[0] directly
send Host / SNI = original hostname # so TLS and virtual hosting still work
verify certificate against original hostname

Most HTTP clients let you supply a custom resolver or a dial hook that receives the resolved address before the socket opens; do the check there, at connect time, on the actual address being dialed. That also covers connection pools and retries, which may resolve again behind your back.

Validate at subscription time as well, so customers get an immediate error for an obviously bad URL. But treat that as a usability check. The security check is the one at connect time, on every delivery, because DNS answers change.


5. Re-Validate Every Redirect

An HTTP client that follows redirects automatically will happily follow 302 Found with Location: http://169.254.169.254/latest/meta-data/ from a perfectly public server. OWASP’s API7 guidance is to disable redirects.

  • For webhooks, do not follow redirects at all. Treat a 3xx as a delivery failure, as the webhooks guide already recommends. The customer can update their endpoint.
  • For fetches where redirects are legitimate (image imports, link previews), follow them manually: take the Location, run it through the full scheme, port and address checks again, cap the number of hops at a small number, and refuse a redirect from https to anything else.

6. Bound Time and Size

A user-supplied URL can point at a server that never finishes responding, or that streams forever. Without bounds, each fetch ties up a worker, and a handful of them is a denial of service against your delivery pipeline.

Bound Why
Connect timeout Unroutable addresses can hang for the OS default, which is far too long
Total request timeout A slow-drip response defeats a per-read timeout
Maximum response size Stop reading at the cap; do not trust Content-Length
Decompressed size A small compressed body can expand enormously; cap after decoding
Allowed content types An avatar import expects an image, not HTML; check what you received
Concurrency per destination One customer’s slow endpoint should not starve everyone else’s deliveries

Pick numbers that fit the feature and publish the ones customers can hit, such as the webhook timeout.


7. Isolate the Fetcher

Application-level checks fail eventually: a new feature bypasses the shared client, a library upgrade changes parsing, a redirect path gets missed. Put a network boundary behind them so that a bypass reaches nothing valuable.

  • Use an egress proxy. Route all user-directed outbound traffic through a forward proxy that performs the address checks itself and is the only path to the internet for that workload. Stripe’s open-source Smokescreen is one example: an HTTP CONNECT proxy that resolves each destination and refuses internal addresses by default.
  • Run fetchers on a separate network segment with no route to internal services, databases or the metadata endpoint. The fetcher only needs the internet; give it only the internet.
  • Harden the metadata service. On AWS, require IMDSv2, which only issues credentials in response to a session token obtained by a PUT request, and keep the response hop limit at 1 where your containers allow it. Azure’s metadata service requires a Metadata: true header and rejects requests carrying X-Forwarded-For; Google Cloud’s requires Metadata-Flavor: Google. These help against the simplest SSRF, where the attacker controls only the URL, but treat them as defense in depth, not as a reason to skip the address checks.
  • Give the fetching workload minimal credentials, so that if metadata is reached, what it yields is worth little.

8. Do Not Echo the Response

The worst SSRF outcomes come from features that return what they fetched. A webhook “test” button that shows the receiver’s response body, or an import that reports the remote server’s error page verbatim, turns a blind SSRF into one that reads internal data.

Return the status code, the timing and a classified error (“connection refused”, “TLS handshake failed”, “timed out”, “destination not allowed”), and nothing from the response body. Make the “destination not allowed” error generic: telling the caller which rule matched helps them map your network. See webhook delivery history for what to record about each attempt, and API security for where SSRF sits among the other OWASP risks.

On this page (9 sections)