Critical Server-side request forgery (SSRF)
At a glance
- Class
- Server-side request forgery (SSRF), tracked as CWE-918
- Standards
- A10:2021 in the OWASP Top 10, API7:2023 in the OWASP API Security Top 10
- Typically reachable from
- Any field or parameter that accepts a URL, a hostname or a file reference
- Primary escalation
- The cloud metadata service, on the link-local address 169.254.169.254 for AWS, Azure and Google Cloud and on a different link-local address for some other providers, and the workload identity credentials it issues: an AWS IAM role, a Google Cloud service account or an Azure managed identity
- How severity is set
- By reach, not by adjacency. A fetch evidenced only by a DNS lookup is low value, proven reach into internal services with response visibility is Medium to High, and retrieval of workload credentials or state-changing access to an internal administrative or orchestration interface is Critical
- Who is affected
- Where the chain reaches workload credentials, everyone whose data the compromised identity can read, commonly beyond the tenant that triggered the fetch. Where it does not, exposure is limited to the internal services the fetch can reach
- Access needed
- Often one low-privilege account, sometimes none where the fetching feature sits on a public upload, preview or sign-up flow
What it is
Server-side request forgery, commonly called SSRF, is a flaw where an attacker supplies a URL and the application's own server fetches it, which turns that server into a proxy for requests the attacker could never send directly. The server sits inside the trust boundary, so it can reach private subnets, internal administrative interfaces, databases, container orchestration endpoints and the cloud metadata service that issues the host's own identity credentials to callers on the host. How far that goes is conditional: whether credentials come back depends on the cloud platform, on how the workload is hosted, and on whether the fetching feature can influence request headers and return any part of the response. The chain below sets out each of those conditions rather than assuming them.
SSRF matters commercially because its severity is set by what the server can reach, not by the feature being abused. A document import field on an insurance claims portal looks harmless in a design review, and the same field can be the shortest path from the public internet to the cloud control plane. OWASP lists SSRF as A10:2021 in the Top 10 and as API7:2023 in the API Security Top 10, and it is tracked as CWE-918. Where the fetch returns credentials, an application bug becomes an infrastructure breach, and the data at risk stops being one record and becomes the whole store.
How the attack unfolds
Mechanism, not a recipe. Reproduction detail stays in client reports, because a page that hands a reader a working attack is a liability.
Any field that accepts a URL lets the attacker choose where the server connects
Claims portals often accept a link instead of an upload: pull a medical report from a hospital portal, fetch a loss adjuster's photographs, import a policy schedule from a broker. The same shape appears in webhook and callback configuration, avatar fetchers, link previews, HTML-to-PDF renderers, image converters and XML or SVG parsers. In each case the attacker chooses the destination and the server, not the browser, opens the connection, which is the request primitive the rest of the chain builds on.
Two properties of the feature decide how far it goes: whether the attacker can influence the request method and headers, and whether anything from the response comes back.
The fetch is pointed at the cloud metadata service on 169.254.169.254
200. These addresses are unroutable from the internet, so they tend to be treated as trusted and left unprotected inside the host. A request arriving from the instance itself is hard to distinguish from a legitimate one, because the service identifies its caller by the fact of being on the host rather than by any credential the caller holds.
Where validation exists at all, it is usually applied to the submitted string rather than to the address finally connected to.
Allowlists and blocklists are walked around with redirects and DNS rebinding
A validator that checks a hostname against an allowlist and then lets the HTTP client follow redirects will deliver the attacker to an internal address on the second hop. A name the attacker controls can resolve into the link-local range, or answer twice with different addresses, a DNS rebinding attack, so that the address checked and the address connected to differ. Alternate IPv4 encodings, decimal, octal, hexadecimal and short forms, together with IPv4-mapped IPv6 notations, defeat naive string matching, though whether they actually connect depends on the HTTP client library and the resolver beneath it.
Deny lists written only for the IPv4 literal also miss the IPv6 metadata endpoint AWS can serve on an IPv6-enabled instance where that endpoint has been switched on.
Whether credentials come back depends on the platform and the hosting shape
On AWS, version 1 of the metadata service, IMDSv1, answers an unauthenticated GET, and the credential document is reached in two requests rather than one: the first lists the role names attached to the instance, the second returns that role's temporary credentials. Three preconditions have to hold, namely that the workload runs on an EC2-style instance, that IMDSv1 is still enabled, and that an instance profile is actually attached, since an instance with no role returns nothing of value. Google Cloud requires a Metadata-Flavor: Google header on every request and has retired its header-free legacy endpoints, and Azure requires a Metadata: true header plus a managed identity assigned to the resource, so a fetch restricted to a plain GET with fixed headers retrieves nothing on either platform.
23, and Lambda exposes no metadata service at all, so reaching those paths means reading the environment or guessing the URI, which is a materially harder step than changing an address.
IMDSv2 raises the bar, and some fetching features clear it anyway
On AWS, IMDSv2 requires a session token obtained with a PUT request carrying a custom header before any credential read, and the default hop limit of one on the instance's metadata options stops a request relayed through an extra network hop such as a container bridge. That defeats a feature restricted to GET with headers it cannot change, and it does not defeat everything: a headless renderer that executes JavaScript, as in HTML-to-PDF conversion and screenshotting, can issue the token request and then the authenticated read, and webhook testers or connectivity-check tools that let an operator choose the method and headers can do the same. CRLF injection into the outbound request, and gopher or dict schemes against a fetcher built on a permissive HTTP client, give the same control by a different route.
Two separate settings keep the older path alive as well: IMDSv1 remains answerable wherever the token requirement is still optional rather than required, which is common in mixed fleets, inherited launch templates, golden machine images and autoscaling groups created before the change, and separately a hop limit raised above one to accommodate containers lets a containerised workload reach whichever version is configured.
The response has to get back out, or the chain stops at proven internal reach
Reading a credential document requires the content of the response, which a great many fetching features never return. Three paths exist: the fetched body is reflected to the user or stored as a retrievable artifact, as in document import, HTML-to-PDF output and image conversion; the content is surfaced indirectly through an error message, a response length or a timing difference; or the fetch follows a redirect the attacker controls, which relays the body to a destination they own. Where none of those exist, a blind fetch still proves internal reach and still matters, and it is not a credential compromise, so it is written up and graded as what was demonstrated.
Repeatability matters for the same reason, since temporary credentials expire and a fetch that fires once cannot be used to renew them.
Credentials are then used from the attacker's own machine, outside the application
Workload credentials are not tied to the host's address unless policy conditions are added to bind them, so they work from anywhere until they expire, and where the fetch can be repeated they can be renewed. The attacker enumerates what the identity permits and reads the object storage holding claim files and medical attachments through the cloud API directly. Nothing in that path touches the application, so its logins, its per-claim authorisation checks and its application audit log record none of it.
The only record sits on the cloud side, in control plane logging and in whatever data event logging has been switched on for the storage itself.
An over-permissioned IAM role or service account turns an application bug into a cloud breach
Application identities are usually broader than the application needs: read access across more storage than one bucket, a secrets store, decryption through a managed key, a queue to publish on, or a trust relationship allowing a further role to be assumed. Each of those extends reach beyond the original documents, and any write permission raises the question of whether claim records and settlement details could be altered rather than only read. Where the identity can issue credentials of its own, or write to configuration the platform reads back, the access can outlive the fix to the fetch.
This step, not the URL field, is what sets the severity of the finding.
Business impact
Where the chain reaches the host's own credentials, the data exposed is the whole corpus rather than a sample of it. A claims document store holds medical reports and diagnostic imaging, loss adjuster photographs of homes and vehicles, bank details used for settlement payments, identity documents, policy schedules and correspondence about claims that were declined. Read through the application, an attacker sees what one account is permitted to see. Read through the storage API with the host's own identity, they see every policyholder, every claim year and every file the retention schedule never removed. Health information in those files is special category personal data under Article 9 of the UK and EU GDPR, which raises both the notification duty and the penalty exposure above an ordinary customer-data loss.
The expensive part is not the extraction, it is the inability to scope it. Object-level data event logging on cloud storage is a chargeable opt-in on the major platforms and is frequently left off, so the record of which files were actually read may not exist, and an insurer that cannot establish which claimants were affected generally has to notify all of them. That decision drives reporting to a supervisory authority within 72 hours under Article 33 of the UK and EU GDPR, notification of data subjects where the risk to them is high, sector reporting under instruments such as the NAIC Insurance Data Security Model Law as adopted by individual US states and DORA for EU financial entities including insurance and reinsurance undertakings, and HIPAA breach notification duties where the organisation is itself a health plan or is handling the data as a business associate for one. Behind the notification sit credit and identity monitoring for an unbounded population, legal costs, regulator engagement and a hard renewal conversation with its own cyber insurer.
Recovery is far wider than the patch. Every credential the compromised identity could reach has to be treated as disclosed and rotated, which means coordinating with each service that consumes those secrets rather than quietly replacing a key. The identity itself usually has to be split and re-scoped, which touches deployment configuration and can require changes in application code written on the assumption of broad access. Metadata service enforcement has to be applied to launch templates, machine images and autoscaling groups rather than to individual instances, or a host replaced during the incident comes back with the old configuration. Where the identity held write permissions, claim and payment records need an integrity review before the business can rely on them, and all of this runs while claims handling continues on the same platform.
How it is found
| Signal | How it is confirmed |
|---|---|
| Every input that accepts a URL, hostname or file reference | Inventory comes first: we work through document import, webhook and callback configuration, avatar and logo fetchers, link previews, HTML-to-PDF and image conversion, XML, SVG and office document parsers, SSO metadata endpoints and feed importers. For each candidate we establish whether the fetch originates from the server or from the browser, because only the first is in this class, and whether the feature lets the request method and headers be influenced, since that decides which escalations are available later. |
| Blind fetches that return nothing to the user | We point a unique hostname per parameter at a listener we control. A DNS lookup on its own is a lead that something server-side resolved the name, and it is not confirmation, because link scanners, mail and URL sandboxes, WAF and CDN resolvers, third-party integrations and our own tooling all produce the same observation. Confirmation is an inbound HTTP or HTTPS request whose source address or ASN is attributable to the target's own infrastructure, or a behaviour in the application response that only the fetch explains. The source address and user agent on that callback also identify which internal component performed the fetch, which is often not the one that received the request. |
| Whether the validation can be walked around | Testing covers redirect following, hostnames that resolve into private or link-local ranges, DNS rebinding between the check and the connection, alternate IPv4 encodings, IPv4-mapped IPv6 notations, the opt-in IPv6 metadata endpoint on AWS, and non-HTTP schemes. We judge the result on where the connection actually landed, never on whether the validator returned an error message. |
| Does a credential path answer, and is it IMDSv1, IMDSv2 or a container endpoint? | We identify the hosting platform first, from cloud marker headers, reverse DNS on the egress address and response fingerprints, then probe that platform's credential path with the headers it requires: AWS IMDSv1 and IMDSv2, the ECS container credential endpoint, EKS pod identity, the Google Cloud metadata host with its required header, Azure IMDS with its required header, and the link-local address used by other providers. A silent 169.254.169.254 is not evidence that no credential path exists. This step presupposes a fetch that returns a response, so with a blind fetch the answer is recorded as undetermined rather than as not reachable. |
| How the finding is graded | Grading follows what was demonstrated, not the escalation that was theoretically adjacent. A blind fetch evidenced only by a DNS callback, with no internal reach shown, is low value and we say so rather than reporting it as a Medium. Proven HTTP reach to internal services with response visibility is Medium to High depending on what those services expose. Retrieval of workload identity credentials, or interaction with an internal administrative or orchestration interface that changes state, is Critical, and a full-read SSRF into an internal admin panel, an orchestrator API or an unauthenticated datastore can be Critical with no credential document anywhere in the chain. |
| What a recovered identity is permitted to do | This step is active interaction with a production cloud control plane, so it is gated: we need written authorisation naming the cloud accounts in scope, plus prior notification so that the client's own security operations team is not running an incident against the test. Where that is in place, we establish how far the access reaches using identity and listing calls and at most a single benign object read, never a bulk read and never a download of regulated content, since a list or head response is sufficient evidence. Every call we make is logged on the client's side and becomes part of their audit record, which is part of why the scope is agreed in writing first. |
How it is fixed
| Control | What makes it hold |
|---|---|
| Move outbound fetching behind one isolated egress service | Application code should not open arbitrary sockets. One fetch service with its own narrow network policy gives a single place to deny loopback in all its encodings, 0.0.0.0, private ranges, the carrier-grade NAT range 100.64.0.0/10, the whole of 169.254.0.0/16 rather than only the metadata literal, IPv4-mapped IPv6 forms and resolution of known metadata hostnames, to restrict schemes to HTTP and HTTPS, and to refuse redirects outright. The isolation is part of the control rather than an extra: the service runs in its own subnet with no route to internal ranges, carries no instance profile or workload identity of its own, and has link-local access blocked at the network layer. Without that, the attacker has merely moved one hop and the same chain runs again from the proxy's position, with the proxy's credentials. |
| Resolve the hostname once, validate every address returned, then connect to one of those | Resolve, check every A and AAAA record against the deny list rather than only the first, and connect to an address already checked instead of resolving a second time, which closes the window DNS rebinding depends on. Connecting to a pinned address means setting the Host header and the TLS SNI to the original hostname explicitly, and the certificate failure that appears when this is missed is routinely fixed by re-resolving the hostname inside the HTTP client, which restores the rebinding window, so the client configuration needs verifying and not only the validator. Where redirects are permitted at all, the full validation runs again on each hop's resolved address. An allowlist of permitted destinations is stronger than a deny list wherever the business can actually enumerate who it needs to fetch from. |
| Treat IMDSv2 and a hop limit of one as containment of one escalation, not as the fix | Requiring the session-token version of the metadata service removes the most damaging outcome, theft of the host's credentials by a fetch that can only issue a GET, and it leaves the SSRF itself untouched: internal administrative applications, unauthenticated caches and search clusters, the orchestrator API, the container credential endpoints it does not govern, and metadata on other platforms in the estate all stay reachable. It holds only where the token requirement is enforced rather than optional, at account or organisation level, and where it is baked into launch templates, machine images and autoscaling groups, since an instance launched later inherits whatever its launch configuration says. A hop limit of one breaks containers that legitimately read metadata through a bridge network, so it survives only where containers are denied the link-local range from their own network namespace and given task or pod identity instead, which is the change to make first. |
| Scope the workload identity and bind it to its network position | One identity per service, no wildcard read across claim-document storage, and no permission the application does not exercise in practice. On AWS the condition keys that actually bind instance credentials are aws:ec2InstanceSourceVPC and aws:ec2InstanceSourcePrivateIPv4, enforced in the role's permission policy and in a service control policy so they cannot be dropped in a single account, whereas aws:SourceVpc and aws:SourceVpce are only populated for requests arriving through a VPC endpoint and aws:SourceIp does not match instance traffic leaving through NAT, so a condition written on those three can look deployed and bind nothing. On Google Cloud the nearest equivalent is a VPC Service Controls perimeter restricting where a service account's calls are accepted from, and on Azure the restriction is generally applied at the resource through storage and key vault network rules rather than to the identity itself, so the limits of each platform need checking before the control is relied on. |
| Make control-plane use of the identity visible | Enable object-level data events on claim-document storage, since they are a chargeable opt-in on the major platforms and are commonly left off, and alert on workload credentials used from an unexpected network or calling an API that identity has never called. It holds because it turns a silent bulk read into a detection, and because it produces the scoping evidence a notification decision depends on. |
Questions
What is SSRF in simple terms?
SSRF, or server-side request forgery, is when an application fetches a URL supplied by a user, so the attacker decides where the server sends a request. The server sits inside the network and can reach what the attacker cannot, including internal admin pages and the cloud metadata service that issues the host's own credentials. The feature looks like a convenience and behaves like a proxy.
Why do SSRF attacks target 169.254.169.254?
169.254.169.254 is the link-local address where AWS, Azure and Google Cloud expose their instance metadata service, and where version 1 of that service is still enabled on AWS it returns the credentials of the IAM role attached to the host to anything on the host that can send a plain GET. The address is unroutable from the internet, so it tends to be treated as trusted and left unprotected. On Azure and Google Cloud the same address answers, but a provider-specific request header is required, which raises the bar without removing it, and some providers use a different link-local address altogether.
Does enabling IMDSv2 fix SSRF?
No. Enabling IMDSv2 closes the most damaging escalation, theft of the host's own credentials, and leaves the SSRF itself in place. The fetch still reaches internal services, administrative interfaces and neighbouring hosts, and IMDSv2 does nothing about any of those, nor about the separate container credential endpoints used by ECS and EKS. It also only holds where version 1 is disabled rather than merely optional, and where the hop limit has not been raised to accommodate containers. Treat it as containment for the worst outcome, not as a fix.
Is blind SSRF worth fixing if nothing comes back?
Yes. Blind SSRF is worth fixing because the response body is not where the value sits. A blind fetch still reaches internal addresses, and plenty of attacks need no output: host and port discovery by response timing, requests to internal endpoints that act on a GET, and cache or queue poisoning. A blind fetch is also often the same code path as a readable one, reached through a different parameter. What it is not is a credential compromise, since reading a credential document needs the content of the response, so the two are graded differently.
Can SSRF lead to remote code execution?
Often, though not through the fetch itself. SSRF reaches internal services that act on whatever they receive: an unauthenticated cache or key-value store, a container orchestration or build API, an internal management endpoint. Where one of those will run a job, write a file or accept new configuration, a server-side request becomes code execution on an internal host. This is why severity is judged on what the server can reach rather than on what comes back in the response.
What is the difference between SSRF and an open redirect?
An open redirect sends the user's browser where the attacker chooses, while SSRF sends the server's own request. The browser case is used for phishing and for stealing tokens out of an authorisation flow, and the server case reaches internal networks and credential services. The two also combine, because a redirect on an allowlisted host is a standard way to get past an SSRF filter.
Can a WAF or a URL blocklist stop SSRF?
Not reliably. A WAF and a URL blocklist both inspect a string, while the vulnerability lives in the connection the server finally makes. A blocked address has decimal, octal, hexadecimal, IPv4-mapped IPv6 and DNS equivalents, a permitted hostname can redirect onward, and a name can resolve differently on the second lookup. Filtering raises the effort and is worth having. The controls that hold are resolving the hostname, validating every address it returns and connecting only to one of those, with outbound fetching confined to a single egress service that refuses redirects, loopback and private ranges.