0
Web Security INTERMEDIATE 14 min read Updated

Server-Side Request Forgery (SSRF): Reaching Internal Services

A practical server-side request forgery (SSRF) guide: make the server fetch URLs for you, reach localhost and cloud metadata, confirm blind cases out of band, and shut it down.

founder & security researcher · 39 writeups
Server-Side Request Forgery (SSRF): Reaching Internal Services

Server-side request forgery (SSRF) is the bug that quietly turns a trusted server into your own fetch bot, making requests you could never make from the outside. You hand a feature a URL, the server loads it, and suddenly your traffic comes from inside the network with all the trust that implies. This guide walks through what the bug really is, the features that tend to carry it, the internal systems it exposes, how to confirm the cases where you see nothing back, the usual filter bypasses at a concept level, a worked lab example, how defenders catch it, and the handful of controls that close it for good.

Read it top to bottom once, then keep it near your methodology notes. Every request shown here targets a lab you run or an application you have written permission to assess.

Scope

Everything below is for a lab you own or a target you are explicitly authorized to test, such as lab.0xpriv.dev or a deliberately vulnerable app on your own machine. Pointing SSRF payloads at systems you do not control, including cloud metadata endpoints of accounts that are not yours, is illegal. Stay in your own yard.

01What server-side request forgery (SSRF) is and why it is dangerous

Server-side request forgery (SSRF) happens when an application takes a URL or host from user input and makes a network request to it on the server side. The value you control becomes the destination of a request the server sends, so you steer where the server reaches. On its own a server fetching a URL sounds harmless. The danger is position: the server usually sits behind the firewall, holds internal DNS, and carries credentials that your browser never will.

That position is the entire prize. A request from your laptop to an internal admin panel dies at the perimeter. The same request, made by a vulnerable server, sails straight through because it starts inside. You are borrowing the server's network identity. Anything it can reach, you can now reach, and anything it is trusted to talk to without authentication is wide open.

why the server's position is the prize
Attackercontrols the URL url= Public appfetches it server side inside the firewall Internal serviceno auth, never exposed

02Where SSRF shows up in real applications

SSRF shows up in any feature whose job is to go fetch something for you. Once you learn the shape, you spot the candidates in seconds, because they all accept a URL or a hostname and then act on it. The common carriers are predictable.

FeatureWhy it fetches a URL
URL fetchers and avatar-by-URLPull a remote image or file and store or display it
WebhooksSend an event to a callback address you configure
PDF and image generatorsRender a page or embed a remote asset you reference
Link previews and unfurlersLoad a link to show a title, thumbnail, or summary
Import by URLGrab a document, feed, or spreadsheet from an address you give

Any of these can take a full URL, a host, a redirect target, or an XML document with an external reference. The input is not always obvious either. A PDF renderer that follows an img tag inside your HTML is fetching a URL you chose, even though the form never asked for one. When you audit an app, list every place it reaches out on your behalf, then ask whether you decide the destination.

03What an attacker can reach through SSRF

What SSRF reaches is everything the server can reach but you cannot, and that list is longer than it first looks. The three that pay off most often are loopback services, internal admin panels, and cloud metadata.

Loopback, meaning 127.0.0.1 and localhost, is where services bind when they are meant for the box only: a database admin tool, a debug port, a cache, an unauthenticated API that trusts anyone who can connect. Internal admin panels live on private ranges and assume the network is the fence. Cloud metadata sits at a fixed link-local address, 169.254.169.254, and on many setups it hands out instance credentials to anything on the box that asks.

Metadata is the jackpot

On a cloud instance with the older unprotected metadata service, a single SSRF read of the credentials path returns temporary keys for the instance role. Those keys often reach storage buckets, queues, and more. This is the path behind several large breaches, so treat 169.254.169.254 as crown-jewel sensitive.

bash
# reach the server's own loopback through the vulnerable fetcher
# 0xpriv.dev/web
curl "https://lab.0xpriv.dev/fetch?url=http://127.0.0.1:6379/"

# reach the link-local cloud metadata address
curl "https://lab.0xpriv.dev/fetch?url=http://169.254.169.254/latest/meta-data/"

Beyond reading, SSRF can poke non-HTTP services if the client is loose about schemes. A fetcher that follows gopher:// or file:// turns into a way to speak raw protocol to a cache or to read local files. That is why scheme control, covered later, matters as much as host control.

04Finding an SSRF candidate and testing it safely

Finding an SSRF candidate starts with the input inventory from earlier: every parameter, header, or body field that looks like a URL, a host, a filename, or a callback. Good names to grep for include url, uri, src, dest, redirect, callback, webhook, image, file, and path. Burp Suite makes this fast: send the request to Repeater and change the value to a host you control.

The first safe test is pointing the parameter at your own listener rather than at anything internal. If the server connects to you, the feature really does fetch attacker-chosen URLs, and you have confirmed the bug before touching a single internal address.

catching the server's own callback
$ curl "https://lab.0xpriv.dev/fetch?url=http://your-listener.example/probe"
(the fetcher replies here)
# on your listener box, a request arrives:
GET /probe HTTP/1.1
Host: your-listener.example
User-Agent: lab-fetcher/1.0

That inbound request is proof. The User-Agent often names the library or service doing the fetch, which tells you which schemes and redirects to expect. From here you move to internal targets, one careful request at a time.

05Basic versus blind SSRF and confirming the blind case

Basic SSRF returns the fetched response to you in the page, so you read the internal service directly in the HTTP reply. You ask for http://127.0.0.1:6379/ and the response body carries whatever that service said. This is the easy case: the oracle is the content itself.

Blind SSRF is the version where the server makes the request but you never see the result. The page says nothing, or it returns a generic success, yet the fetch still happened somewhere you cannot read. You confirm it out of band: point the parameter at a host you control and watch for the interaction on your side rather than in the HTTP reply.

An out-of-band interaction is simply the server reaching your canary. Tools such as Burp Collaborator give you a unique subdomain, and when the target resolves or requests it, you see the DNS lookup and any HTTP hit land on your dashboard. A DNS lookup alone proves the server parsed and tried your host, even when no connection follows, which is often enough to confirm a blind case.

confirming blind SSRF out of band
Fetcherreply is blank resolves canary Canary hostyou control it DNS and HTTP log Confirmedinteraction seen
http
POST /webhook/test HTTP/1.1
Host: lab.0xpriv.dev
Content-Type: application/json

{"callback":"http://abc123.oob.your-canary.example/hook"}

06Filter bypasses, explained at the concept level

Filter bypasses matter because most teams block SSRF with a deny list, and deny lists leak. If the app only checks the literal string 127.0.0.1 or 169.254.169.254, there are many other ways to name the same destination. The point of knowing these is to test whether a filter is real, not to ship a bypass kit, so this stays conceptual.

Bypass ideaWhy it slips a weak filter
Alternate IP encodingsDecimal, octal, or hex forms of an address resolve to the same host but do not match a string check
Open redirectsAn allowed URL that 302s onward lands the fetcher on an internal host the filter never saw
DNS rebindingA name that answers with a public IP at check time and a private IP at fetch time passes validation, then points inward
Alternate hostnamesNames that map to loopback, or trailing dots and odd casing, dodge exact-match lists

Alternate IP encodings lean on the fact that an address has many spellings. The decimal form 2130706433 is the same as 127.0.0.1, and a parser that resolves it while the filter only compares strings is fooled. Open redirects exploit a gap between validation and fetch: the app approves a harmless allowed URL, that URL redirects, and if the fetcher follows redirects it ends up somewhere no one approved.

bash
# four spellings of the same loopback host, used to test a string filter
# 0xpriv.dev/web
http://127.0.0.1/        # the obvious one a deny list blocks
http://2130706433/       # decimal form of 127.0.0.1
http://0177.0.0.1/       # octal first octet
http://127.1/            # short form, still loopback

DNS rebinding is the sharpest of the three. The attacker controls a domain and makes it answer with a public address when the application validates the host, then flips the same name to a private address, such as 127.0.0.1, before the fetch runs. Validation and fetch resolve the name separately, so the check sees one answer and the request lands on another. It beats host allow lists that validate the name instead of the final connected IP.

Test the filter, do not just trust it

If a parameter rejects 127.0.0.1, try an alternate encoding or a name that resolves to loopback before you call it safe. A deny list that only blocks the obvious strings is not a control, and proving that in a report is far more useful than assuming it works.

07A worked example against lab.0xpriv.dev reaching an internal service

This worked example puts the pieces together against an avatar-by-URL feature on lab.0xpriv.dev, in a lab built for exactly this. The feature takes an image URL, the server fetches it, and it stores the result as your profile picture. That is a textbook SSRF carrier: you choose a URL, the server loads it, and it shows you part of the result.

Confirm the fetch is yours to steer

Set the avatar URL to a listener you control and watch the inbound request land. The server connects out, so the destination really is attacker chosen.

Probe loopback

Swap in http://127.0.0.1 on common ports. An internal service answers where the public internet never would.

Read the internal service

Point at the admin or metadata endpoint. If the feature echoes any of the fetched body, you read the response directly.

Pivot with what you learn

Use tokens, hostnames, or config leaked from one read to reach the next internal target, staying inside the lab the whole time.

stepping from loopback to an internal admin
$ # step 1: internal service on loopback answers
$ curl -s "https://lab.0xpriv.dev/avatar?url=http://127.0.0.1:8080/"
<title>Internal Dashboard</title> ... Sign in not required on localhost
$ # step 2: the cloud metadata role path, read through the same fetcher
$ curl -s "https://lab.0xpriv.dev/avatar?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
lab-instance-role

In the lab, the avatar fetcher happily loads 127.0.0.1:8080 and returns the internal dashboard markup in the response, because that service assumed only the host itself would ever connect. The same fetcher reads the metadata path and names the instance role. A real engagement would stop, document the reachable targets, and report, because the finding is already proven. The lesson is the pattern: a feature that fetches a URL plus a service that trusts the network equals a straight line to the inside.

Do not weaponize beyond proof

Confirming you can read an internal service is the finding. Pulling live credentials, hitting production data, or chaining into real infrastructure is out of scope on anything you are not authorized to fully exploit. Prove reach, capture evidence, report, stop.

08Detecting SSRF from the defender's side

Detecting SSRF relies on watching where the server itself connects, because the attack is an outbound request from a box that should have a short, predictable list of destinations. The strongest signal is an application server suddenly connecting to 169.254.169.254, to loopback ports it has no reason to touch, or to private ranges outside its normal path. None of that belongs in a web server's egress profile.

Log the destination host, port, and scheme of every outbound fetch a URL-handling feature makes, and alert when the destination is link-local, loopback, or a private range. On the network side, egress logs from the app tier that show connections to the metadata address or to internal admin ports are high-confidence indicators. Pair that with request logs: a user-supplied URL field whose value is an IP in an odd encoding, or a hostname that resolves to a private address, is worth flagging before the fetch even runs.

09How to shut SSRF down

Shutting SSRF down comes down to never letting raw user input decide where a server-side request goes, backed by network controls for the day a check is missed. The fixes stack, and the first two do most of the work.

python
# validate against an allow list, then connect to the resolved IP you checked
# 0xpriv.dev/web
import ipaddress, socket

ALLOWED_HOSTS = {"images.lab.0xpriv.dev", "cdn.lab.0xpriv.dev"}
ALLOWED_SCHEMES = {"https"}

def safe_target(scheme, host):
    if scheme not in ALLOWED_SCHEMES:
        raise ValueError("scheme not allowed")
    if host not in ALLOWED_HOSTS:
        raise ValueError("host not on allow list")
    ip = ipaddress.ip_address(socket.gethostbyname(host))
    if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
        raise ValueError("resolved to an internal address")
    return ip   # connect to this exact IP, not by re-resolving the name
How to shut it down
Allow list hosts and schemes. Accept only the handful of destinations and schemes the feature truly needs, and reject everything else by default rather than blocking known-bad strings.
Block link-local and private ranges. Resolve the host, check the IP, and refuse loopback, private, link-local, and reserved addresses, including 169.254.169.254 and 127.0.0.1.
Disable redirects you do not need. If the fetcher does not require following 3xx responses, turn them off so an approved URL cannot bounce the request to an internal host.
Keep raw user URLs away from internal fetchers. Route outbound fetches through a hardened egress proxy and never pass a user-controlled URL straight to an internal HTTP client.
Lock down egress and metadata. Restrict which destinations the app tier can reach at the network layer, and enforce the protected metadata service (IMDSv2) so a single read cannot hand out credentials.

Validate against the connected IP, not the name you were given, or DNS rebinding walks right past the check. The code above resolves the host once and would connect to that exact IP, which closes the gap between validation time and fetch time.

10FAQ

Is SSRF only an HTTP problem?FAQ

No. HTTP and HTTPS are the common cases, but a fetcher that accepts other schemes can reach far more. A client that follows file:// can read local files, and one that follows gopher:// can send crafted bytes to services that speak plain-text protocols. That is why controlling allowed schemes is as important as controlling allowed hosts.

Why is the cloud metadata address such a big deal?FAQ

The link-local metadata service at 169.254.169.254 can return temporary credentials for the instance role. If SSRF reads that path on an unprotected setup, the attacker gets keys that often reach storage, queues, and other services. Enforcing the protected version (IMDSv2) and blocking link-local egress removes that single-request path.

How do I confirm SSRF when the page shows nothing?FAQ

Use an out-of-band interaction. Point the parameter at a unique canary host you control, such as a Burp Collaborator subdomain, and watch for the DNS lookup or HTTP request to arrive on your side. The interaction proves the server reached your host even though the HTTP reply gave nothing away.

Does blocking 127.0.0.1 fix SSRF?FAQ

Not by itself. A string block on 127.0.0.1 is bypassed by alternate encodings like the decimal 2130706433, by names that resolve to loopback, and by DNS rebinding. Real protection resolves the host and checks the actual IP against loopback, private, link-local, and reserved ranges, and connects to that checked IP.

What is the single most effective defense?FAQ

An allow list of the exact hosts and schemes the feature needs, enforced against the resolved IP. Everything else, from disabling redirects to tightening egress, is defense in depth around that one decision: the server, not the user, chooses where the request goes.

11Wrapping up

Server-side request forgery is powerful for one plain reason: it lets you borrow a server's place in the network. Find the features that fetch URLs, confirm the fetch is yours to steer, and the inside of the network opens up, from loopback services to internal panels to cloud metadata. Confirm the blind cases out of band, test filters instead of trusting them, and prove reach rather than weaponizing it. On the defense side, the cure is short and boring in the best way: let the server decide the destination through an allow list checked against the real IP, block the ranges that matter, and keep raw user URLs away from anything that reaches inward.

references

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Web →

discussion

0 comments
`code` supported · comments are reviewed before they appear