Skip to content

Server-side request forgery (SSRF)

A server that fetches a user's URL must judge the address it connects to, not the URL text: resolve, refuse non-public ranges, re-check redirects.

Predict first

Before you read: predict

ExamHall lets a university admin set the URL its attempt.graded webhook is sent to. Before each send, ExamHall's sender checks the URL's hostname against a denylist of names (localhost, metadata) and, if it passes, fetches it. An admin sets the URL to the cloud instance-metadata address written as a plain address, http://169.254.169.254/latest/meta-data/.

With this check in place, what happens when ExamHall sends?

Pick one

The 30-second version

SSRF is when your server fetches a URL a user controls, so the attacker uses your server to reach internal services and the cloud metadata address. Checking the URL text is not enough: resolve the host and refuse private, loopback and link-local addresses, and prefer an allow-list of known hosts.

A server that fetches a user's URL must judge the address it connects to, not the URL text: resolve, refuse non-public ranges, re-check redirects.

How it works

Step 1 of 11

SSRF is a server fetching a URL a user supplied — a webhook target, image import or preview renderer — reaching what the attacker cannot.
In the kitchen
  • a visitor writes an office on an errand note → the user-supplied URL (steps 1, 3)
  • the runner carries a master keycard to every floor, the server room and records vault included → the server's network position and privileges (steps 1–2)

Analogy: An office tower's internal errand runner and the floor directory (facilities)

In the tower In the server
a visitor writes an office on an errand note the user-supplied URL (steps 1, 3)
the runner carries a master keycard to every floor, the server room and records vault included the server's network position and privileges (steps 1–2)
a desk clerk who only checks the office name against a short banned-names list a hostname or string check that never sees the real room (step 3)
looking the name up in the authoritative floor directory and refusing any staff-only floor resolve the address and refuse private and link-local ranges (step 4)
walking to that exact room number, not re-reading the directory board on the way connect to the pinned, validated address (step 5)
a note that says "then take it onward wherever they send you", re-checked at each office re-check every redirect hop (step 6)
everything carried out leaves through the one goods-out gate with a logbook an egress proxy (step 10)
the records vault opens only to a runner holding a fresh signed chit IMDSv2's session token at the resource (step 9)

In real terms: judge the room the note actually points to, not the words on the note, and refuse the staff-only floors every time — even when a later note sends the runner onward.

Breaks down when: a runner can refuse a note that merely feels wrong. Software applies the rule mechanically, so the rule must be exact and deny by default (step 4), not a short list of names anyone can route around.

Contrasting case: webhook verification (api-webhooks-hmac-retries) guards an inbound request the server receives; SSRF guards the outbound fetch the server itself makes from a user's URL.

Examples, run for real

Every line below is the guard's observed decision in the lab. A "reached" line means a local fixture answered; a "refused" line means the guard stopped before connecting. No real address is contacted.

4.1 EXH-3790: the metadata webhook, name check vs resolved-address check

ExamHall's admin-set webhook URL points at the link-local metadata address. The served name-denylist check lets it through; MR !4090 resolves the host and refuses the address, and still sends to a university's public webhook URL (steps 3–4).

4.1 EXH-3790 name-denylist check   reached=true range=link-local reason=reached
4.1 MR !4090 resolve + range check  reached=false range=link-local reason=blocked-ip
4.1 MR !4090 public webhook URL     reached=true range=public reason=reached

A name list never sees that 169.254.169.254 is a link-local address; resolving and judging the address does, and refuses it.

4.2 Chat preview fetcher: checking one address vs re-checking every redirect

A chat link-preview fetcher that checks only the first address follows a 302 into the internal network; the fix re-checks each hop, while still following a legitimate public redirect (steps 5–6).

4.2 first-address-only, redirect    reached=true range=private reason=reached
4.2 re-check each hop, redirect      reached=false range=private reason=blocked-ip
4.2 re-check each hop, public hop    reached=true range=public reason=reached

The first address being public does not make the redirect target safe; re-checking the hop refuses the internal address while a redirect to a public host is still allowed.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A chat app builds previews of the URLs its users post. It is tested with two URLs on links.chat.example (93.184.216.34): one answers 302 to http://cdn.chat.example/, a public host at 93.184.216.35; the other answers 302 to http://internal.metrics.local/, at 10.0.0.12. Every guard below range-checks the first address.

Which one follows the first redirect and refuses the second?

Pick one

Question 2 of 7

A chat app's "import avatar from URL" feature checks only that the URL begins with http(s). Ops find it reached 169.254.169.254 and admin.intranet (192.168.10.9) after users pasted them. Every avatar so far came from api.chat.example (93.184.216.34). Each fix is tested with those two URLs, with internal.metrics.local (10.0.0.12), which ops have not seen, and with cdn.chat.example (93.184.216.35), a public host no one has used yet.

Which fix reaches none of 169.254.169.254, 192.168.10.9 and 10.0.0.12, and still imports from cdn.chat.example?

Pick one

Question 3 of 7

A chat fetcher resolves a URL's host, checks the address is public, and — if it passes — opens the connection by looking the hostname up again. For one import the host's record returns a public address when the fetcher checks it and a private address a moment later when it connects.

Does the fetch reach the private service?

Pick one

Question 4 of 7

A chat app may fetch previews only from its two partner media hosts, partner.cdn.example and media.partner.example. DNS answers 93.184.216.34 for partner.cdn.example and for api.chat.example, which is not a partner; media.partner.example's record now points at 192.168.10.9.

Which design fetches from partner.cdn.example, refuses api.chat.example and never reaches 192.168.10.9?

Pick one

Question 5 of 7

A teammate writes: "Our image-import endpoint already rejects any URL whose hostname is on our block-list, so we're safe from SSRF".

Write three sentences that answer them.

Question 6 of 7

An MR adds an SSRF guard to a chat "import image from URL" endpoint: it resolves the host, checks the first address is public, then downloads by handing the original URL to an HTTP client that resolves the host again and follows redirects with no further check.

Which review comment closes the hole?

Pick one

Question 7 of 7

It receives five user URLs, listed with DNS's answers and one redirect: (1) http://img.chat.example/a.png, where img.chat.example is 93.184.216.34; (2) http://169.254.169.254/latest/meta-data/; (3) http://status.chat.example/, where status.chat.example is 10.0.4.2; (4) http://links.chat.example/go, where links.chat.example is 93.184.216.34 and answers 302 with Location: http://backup.chat.example/, which is 172.16.5.4; (5) ftp://files.chat.example/avatar.png.

How many of the five does it refuse?

Pick one

Go deeper

Pro tips
  1. In review, list every place the server fetches a URL a user can set. When: any webhook target, image import or preview renderer (steps 1, 11). Trace where each URL comes from before judging its guard.
  2. Log each refused fetch with the range it resolved to. When: any server-side fetch of a user URL (step 11). A refusal log shows which ranges users' URLs point at, and a burst of them is the alert. Verify: the guard's refusal carries its range (range=link-local reason=blocked-ip).
Sources

Valid as of 2026-10-19.

  • OWASP, Server Side Request Forgery Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html · fetched 2026-10-19 (owasp-ssrf-prevention.txt, sha256 33e9336bfff8…). "the application must be able to detect, at the code level, that the provided IP (V4 + V6) is not part of the official private networks ranges including also localhost and IPv4/v6 Link-Local addresses." Supports steps 3–6, 8; its passages on rebinding, allow-lists and connecting only to validated addresses are in the assessment note below. Valid as of 2026-10-19.
  • AWS, Use IMDSv2 (EC2 User Guide, Instance metadata and user data): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html · fetched 2026-10-19 (aws-imdsv2.txt, sha256 57dfe5af625f…). "IMDSv2 uses session-oriented requests. With session-oriented requests, you create a session token that defines the session duration…" and "The examples in this section use the IPv4 address of the Instance Metadata Service (IMDS): 169.254.169.254." Supports steps 2, 9. Valid as of 2026-10-19.
  • IETF, RFC 1918 — Address Allocation for Private Internets: https://www.rfc-editor.org/rfc/rfc1918.txt · fetched 2026-10-19 (rfc1918.txt, sha256 debc89dcd9c8…). IANA reserves "10.0.0.0 - 10.255.255.255 (10/8 prefix)", "172.16.0.0 - 172.31.255.255 (172.16/12 prefix)" and "192.168.0.0 - 192.168.255.255 (192.168/16 prefix)". Supports step 4.
  • IETF, RFC 3927 — Dynamic Configuration of IPv4 Link-Local Addresses: https://www.rfc-editor.org/rfc/rfc3927.txt · fetched 2026-10-19 (rfc3927.txt, sha256 44ab8b94826b…). "The IPv4 prefix 169.254/16 is registered with the IANA for this purpose." Supports steps 2, 4.
  • Cross-pack: api-webhooks-hmac-retries owns the webhook sender's signing and retries (this lesson guards its outbound fetch); sec-owasp-map owns where the Top 10 files SSRF; sec-secrets-management owns secret storage and rotation. This lesson links them and does not re-teach them.

Checked against the tools and docs as of 2026-10-19.