Lesson 4 of 11 · 8 min read
Server-Side Request Forgery
In this lesson
TL;DR: In SSRF the server makes a request to a destination the attacker chose, using the server's network access: internal APIs, databases with HTTP interfaces, admin panels, and the cloud metadata service that hands out credentials. URL string checks fail against alternative IP encodings, DNS tricks and redirects. Defend by allowlisting destinations where possible, otherwise by resolving the host, rejecting non-public addresses, and connecting to the address you checked, re-validating every redirect, and sending fetches through an egress proxy in a network segment with no internal access. Agent browsing and fetch tools need all of this.
Before you read
Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.
1A link-preview feature fetches any URL a user posts and shows the page title. Why is that dangerous on a cloud server?
The server as an unwitting proxy
Many features fetch URLs: link previews, webhooks, PDF and image importers, "import from URL", integrations, and now LLM tools that browse or fetch documents. Each runs inside your network with access the requester lacks. If the requester controls the destination, the server becomes a confused deputy for network access.
Targets attackers aim for:
- Cloud metadata services that return instance details and temporary credentials.
- Internal APIs and admin panels that trust anything on the internal network.
- Datastores with HTTP interfaces (search engines, caches, container and orchestration APIs).
- Localhost services on the same machine.
When the response is not returned to the attacker, the SSRF is blind, but it can still trigger actions on internal services or reveal information through timing and errors.
Why string checks fail
Blocking URLs that contain localhost or 169.254.169.254 misses:
| Bypass | Example |
|---|---|
| Alternative IP encodings | Decimal 2130706433 or hex 0x7f000001 for 127.0.0.1 |
| IPv6 forms | [::1], IPv4-mapped IPv6 addresses |
| DNS names that resolve internally | An attacker's domain with an A record of 10.0.0.5 |
| DNS rebinding | A name that resolves to a public IP at check time and an internal one at fetch time |
| Redirects | A public URL that responds with a redirect to an internal address |
The only reliable check is on the IP address the connection actually goes to.
Defences in order of strength
- Allowlist destinations. If the feature only needs a few partner domains, allow exactly those and nothing else.
- Isolate the fetcher. Run outbound fetches through an egress proxy, or in a network segment that cannot reach internal ranges or the metadata service at all. This holds even if validation code has a bug.
- Validate the resolved address, then pin it. Resolve the host, reject any non-public address, and connect to the exact address you validated, so a second DNS lookup cannot change the answer.
- Handle redirects. Disable them, or re-run the full validation on every hop.
- Harden the metadata service. AWS IMDSv2 requires a session token obtained with a PUT request and limits hop count, which blocks most SSRF paths to credentials; other clouds require specific headers.
- Limit what comes back. Return only what the feature needs (a title, a thumbnail), not raw responses or detailed errors.
Here is the address check from step 3, using Python's ipaddress module:
import ipaddress
import socket
from urllib.parse import urlsplit
ALLOWED_SCHEMES = {"http", "https"}
def resolve_public(url: str) -> tuple[str, str]:
"""Return (host, ip) if every address the host resolves to is public."""
parts = urlsplit(url)
if parts.scheme not in ALLOWED_SCHEMES or not parts.hostname:
raise ValueError("scheme or host not allowed")
port = parts.port or (443 if parts.scheme == "https" else 80)
infos = socket.getaddrinfo(parts.hostname, port, proto=socket.IPPROTO_TCP)
ips = {info[4][0] for info in infos}
for ip in ips:
addr = ipaddress.ip_address(ip)
if not addr.is_global: # private, loopback, link-local, reserved...
raise ValueError(f"{parts.hostname} resolves to non-public {ip}")
# Connect to this exact IP (sending the original Host header), so a
# second DNS lookup cannot return a different, internal address.
return parts.hostname, sorted(ips)[0]
Tested against loopback, metadata, private ranges, [::1], and the decimal and hex encodings of 127.0.0.1, it rejects all of them and accepts a public address. It is one layer: pair it with network isolation, because validation code is where bypasses are found.
Metadata services across clouds
Every major cloud gives instances a link-local metadata endpoint that hands out the instance's role credentials, which makes it the classic SSRF target. Each provider hardened it against the same observation, spelled out in AWS's security blog when IMDSv2 launched: most SSRF primitives can only issue a GET to a URL, without custom headers, and misconfigured reverse proxies add an X-Forwarded-For header to what they relay.
| Provider | What a request must include | What is rejected |
|---|---|---|
| AWS IMDSv2 | A session token from PUT /latest/api/token (TTL 1 to 21,600 seconds), sent back in X-aws-ec2-metadata-token | Requests without a valid token when tokens are required; token PUTs carrying X-Forwarded-For; token responses beyond the hop limit (default 1) |
| Google Cloud | The header Metadata-Flavor: Google | Requests without it, and requests with X-Forwarded-For |
| Azure | The header Metadata: true | Requests without it, and requests with X-Forwarded-For |
AWS also offers an account-level default that makes new instances IMDSv2-only (since March 2024), and an IPv6 metadata address on Nitro instances, which is one more reason string blocklists of 169.254.169.254 fail.
Worked example: which SSRF primitive reaches credentials?
| Primitive | AWS IMDSv1 | AWS IMDSv2 required | Google Cloud | Azure |
|---|---|---|---|---|
| URL-only GET (a link preview fetcher) | Credentials | Blocked: no PUT, no token | Blocked: no header | Blocked: no header |
Open reverse proxy that adds X-Forwarded-For | Credentials | Blocked: XFF on the PUT | Blocked: XFF | Blocked: XFF |
| HTTP client whose method and headers the attacker controls (for example a model-driven HTTP tool) | Credentials | Credentials, within the hop limit | Credentials | Credentials |
URL-only GET fetcher
Attacker supplies a URL
GET to metadata
no token, no header
Refused
Outcome, safe: IMDSv2 and headers stop it
The primitive cannot do a PUT or set headers.
Model-driven HTTP tool
Injected text steers the model
PUT for token, then GET
method and headers chosen
Role credentials returned
Outcome, unsafe: Hardening is bypassed
A capable client satisfies every metadata check.
Same tool, isolated network
Injected text steers the model
No route to metadata
egress tier, public only
Connection fails
Outcome, safe: Contained by the network
Works even when the client is fully capable.
The common primitive
Most SSRF bugs can only send a GET to a URL. Token and header requirements defeat them.
The first two rows are why metadata hardening was worth doing: it kills the common primitives. The third row is why it is not enough for agents. A tool that lets the model choose the method and headers is a fully capable HTTP client, and only network isolation of the fetcher (no route to the metadata address at all) plus a minimal role covers it.
The 2019 Capital One breach is the reference case for the combination: a server-side request forgery through a misconfigured web application firewall reached the metadata service of the time, the role it returned could list and read many storage buckets, and nothing restricted where the data could be sent. Teach it as three failures (SSRF, an over-privileged role, no egress control), not as one.
Webhooks and blind SSRF
Webhooks invert the usual feature: the customer chooses a URL and your servers call it, forever, on every event. Treat the delivery system as hostile-input infrastructure:
- Deliver from a separate egress tier whose resolver and firewall only reach the public internet, never internal ranges or metadata, and re-check the resolved address on every delivery, not just at registration (a DNS record can change after you validated it).
- Never return the target's response body to the customer, and keep error messages generic; status codes and timing are enough to map an internal network if you echo them.
- Sign payloads with an HMAC per customer so receivers can verify them, and cap redirects, response size and time.
Blind SSRF, where the attacker never sees a response, is still serious: a request alone can trigger internal state changes on services that act on GETs, and differences in timing or error codes leak which internal hosts and ports exist.
SSRF from the browser to localhost
The same confused-deputy pattern runs inside the user's browser. A web page can make the browser send requests to localhost or a private address, and developer tools increasingly run local HTTP servers: notebook servers, model runners, MCP inspectors and proxies. If such a service has no authentication and accepts requests from any origin, any website the developer visits can drive it.
CVE-2025-49596 in the MCP Inspector is the agent-era example. Its proxy listened on all interfaces on port 6277 without authentication, so a malicious page could reach it, through the browser's handling of 0.0.0.0 or through DNS rebinding, and launch commands. Version 0.14.1 fixed it by adding a session token and checking the Origin and Host headers. Browsers are adding a backstop: Chrome 142 introduced Local Network Access, which asks the user's permission before a public site can send requests to local or loopback addresses. Treat that as defence in depth; the service itself must authenticate, bind to loopback only, and check Origin and Host.
Agents and fetch tools
Web page
Says: fetch an internal URL
Agent
Has a fetch tool
Metadata service
Cloud credentials
Controls
Egress proxy
- Public addresses only
Also in the picture
Internal admin API
Fetch tool with direct network access
Injected text asks the agent to fetch an internal address. The tool runs inside the network and reaches the metadata service and an admin API.
A browsing or fetch tool exists to request URLs chosen by the model, and the model's choices can be steered by any text it reads. That makes the tool an SSRF primitive by design. It needs every defence above, enforced in the tool or the network rather than in the prompt, plus the reverse concern: the same tool is an exfiltration channel if it can put private data into a URL.
Misconceptions interviewers listen for
- "IMDSv2 fixes SSRF." It stops the common primitives against one target. Internal APIs, other metadata paths and fully capable HTTP clients remain.
- "Block 169.254.169.254 and localhost by string." Alternative encodings, IPv6, DNS names, redirects and rebinding all bypass it.
- "Blind SSRF is low severity." Requests alone can change internal state and map the network.
- "Services on localhost are safe from the web." Browsers can reach them; authentication and Origin checks are required.
- "OWASP dropped SSRF from the Top 10." It was folded into A01 Broken Access Control in the 2025 edition, alongside CSRF, because both are a server or browser acting with authority the requester lacks.
What if · interview follow-ups
Hold your answer as the constraints change
Your product has a link-preview feature, customer-configured webhooks and an AI assistant with a fetch tool, all running on cloud instances. The interviewer probes the server-side request paths one at a time.
Start here
What if the link-preview fetcher follows redirects?
Where was the address validated, and where does the final connection go?
A public URL that redirects to an internal address bypasses a check done only on the first URL. Disable redirects, or re-run resolve, validate and pin on every hop with a small hop limit. Run the fetcher in an egress tier with no route to internal ranges or the metadata service, so a validation bug still fails closed.
Carry it forwardValidate the address you actually connect to, every time, and back it with network isolation.
Weak answerValidating the user-supplied URL string once.
Constraint changes
What if the instances still allow IMDSv1?
What does a URL-only GET primitive reach?
Any SSRF that can issue a plain GET, including the link previewer, can read the instance's role credentials. Require IMDSv2 across the account, set the hop limit to 1 where containers should not reach metadata, and scope each role to what that service needs. Then hunt for use of the existing role credentials from unexpected addresses.
Carry it forwardMetadata hardening removes the cheapest path from SSRF to cloud credentials.
Weak answerAssuming SSRF only matters if internal admin panels exist.
Constraint changes
What if a customer points a webhook at a hostname that resolves to a public IP today and an internal IP next week?
When is the address checked?
Validation at registration time is stale by the next delivery. Resolve and validate on every delivery and connect to the checked address, from a delivery tier whose network cannot reach internal ranges anyway. Return no response bodies and generic errors so the webhook cannot be used to map the internal network.
Carry it forwardUser-chosen destinations must be re-validated at use time, not configuration time.
Weak answerTreating webhook URL validation as a one-time form check.
Constraint changes
What if the AI assistant's fetch tool lets the model choose the HTTP method and headers?
Which metadata protections depend on the attacker lacking control of headers?
All of them: with control of method and headers, the tool can perform the IMDSv2 token PUT or add the Google and Azure metadata headers, and indirect prompt injection can steer the model to do it. Remove the network route from the tool's runtime to metadata and internal ranges, give it a separate minimal identity, restrict methods to GET, and allowlist destinations where the use case allows.
Carry it forwardA model-driven HTTP client is a fully capable SSRF primitive; only network isolation and least privilege contain it.
Weak answerRelying on IMDSv2 to protect an agent's HTTP tool.
Constraint changes
What if a developer's local MCP or notebook server listens on a port without authentication?
Can a website the developer visits send requests to it?
Yes: the browser can be made to send requests to localhost or 0.0.0.0, and DNS rebinding can make them same-origin, so any page can drive the tool and whatever it can run. The service must bind to loopback, require a session token, and check Origin and Host. Browser Local Network Access prompts help but are a backstop, not the control.
Carry it forwardLocalhost is a network location, not an authentication mechanism.
Weak answerAssuming a service is private because it runs on the developer's laptop.
Now defend it
As the defender, what would tell you SSRF is being attempted?
Egress-tier denials for internal or link-local destinations, metadata-service access from processes or containers that should not use it, role credentials used from addresses outside the instance or its VPC endpoint, webhook targets whose resolution changes from public to private, and fetch-tool requests to unusual ports or methods in agent logs.
Primary references
References for the mechanisms discussed above, accessed 6 November 2026:
- OWASP SSRF prevention: destination allowlists, DNS and redirects.
- AWS IMDSv2 and hop-limit behavior.
How this shows up in interviews
"What is SSRF, and why do LLM fetch tools bring it back?" is a frequent question for AI security roles. Strong answers name the metadata service and internal APIs as targets, explain why string filters fail, describe resolve-validate-pin plus network isolation, and mention IMDSv2. Then they connect it to agents: the model chooses the URL, injection chooses for the model, so the tool must enforce destination rules itself.
Say it in 30 seconds: "SSRF is the server fetching a URL an attacker influenced, from inside our network, so it can hit internal APIs or the cloud metadata service for credentials. String filters fail against encodings, DNS tricks and redirects. I allowlist destinations where I can; otherwise resolve, reject non-public IPs, connect to the checked IP, re-validate redirects, and run fetches through an egress proxy that can't reach internal ranges. Agent fetch tools are SSRF by design, so the tool enforces this, not the prompt."
Check yourself
Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.
1Your validation resolves the hostname, confirms the IP is public, then passes the original URL to an HTTP library, which resolves it again. What attack does this allow?
2Why is network isolation of the fetcher worth having even with perfect validation code?
3Why do Google Cloud and Azure reject metadata requests that carry an X-Forwarded-For header?
4A webhook URL was validated as public when the customer registered it. Why is that not enough?