SSRF

SSRF: the bug that turns your server into the attacker

Your application has a feature that fetches a URL. That feature is standing inside your network, and it does what it is told.

In March 2021, Microsoft shipped out-of-band patches for a set of Exchange Server flaws that were already being exploited at scale. The one at the front of the chain, CVE-2021-26855, was a server-side request forgery bug: an unauthenticated attacker could make the Exchange server send HTTP requests of their choosing, and have those requests arrive wearing the server's own identity.

Everything that made that incident infamous — the web shells, the mass scanning, the months of incident response — happened downstream of one capability: the ability to make someone else's server issue a request on your behalf. That capability is still being handed out by applications shipping today, usually by a feature that looked completely harmless in review.

CVE
CVE-2021-26855, commonly called ProxyLogon
Product
Microsoft Exchange Server 2013, 2016 and 2019 (on-premises). Exchange Online / Microsoft 365 was not affected.
Class
Server-side request forgery (CWE-918)
Severity
CVSS 9.8 (Critical) per NVD
Disclosed
2 March 2021, in out-of-band patches — already under active exploitation, and later added to CISA's Known Exploited Vulnerabilities catalogue
Impact
Unauthenticated attacker-controlled requests issued as the server, and authentication as the server; chained with further Exchange flaws to reach remote code execution and persistent web shells

The root cause, in one sentence

The application accepted a destination from the caller and then made a request to it from a position of trust the caller did not have.

That is the whole of SSRF. Your server sits somewhere valuable: inside the network perimeter, holding an internal DNS view, carrying a cloud instance role, trusted by services that never expected to be asked for credentials. A feature that fetches a caller-supplied URL quietly lends all of that out, one request at a time. The attacker does not need to get inside your network, because you have politely offered to go in and fetch things for them.

No request is published here. As with our last post, the mechanism is described at the level a defender needs and no further. If you still run Exchange on-premises, your action is to confirm your build number against Microsoft's advisory — and, because this one was mass-exploited, to assume compromise and go looking rather than simply patching and moving on.

Where this lives in applications built today

Exchange is not the lesson. The lesson is that every URL your application fetches on a user's say-so is an SSRF candidate, and most applications have more of them than the team remembers. These are the five we find most often.

1. The obvious URL field

Webhooks, "import from URL", avatar-by-URL, RSS and feed readers, link previews, integration callbacks, "test this endpoint" buttons in admin settings. These are honest, documented features. They are also a request issued from your infrastructure to an address the user chose, which is the definition of the bug. Webhook configuration is the single most productive place we look, because it is usually admin-only and therefore reviewed as though it were safe.

2. Document and image renderers

HTML-to-PDF converters, headless browsers generating screenshots or invoices, SVG processing, XML parsers with external entities enabled, office-document thumbnailers. The user does not supply a URL at all here — they supply a document, and the document supplies the URL. Teams rarely think of "upload an invoice template" as a network feature, which is exactly why it survives review.

3. The cloud metadata endpoint

This is what turns a medium-severity curiosity into a critical finding. Cloud instances expose an internal metadata service — on AWS and others at the link-local address 169.254.169.254 — which hands out information about the instance, and on older configurations can hand out temporary credentials for the role the instance is running as. An SSRF that reaches it stops being "the server fetched a URL" and becomes "the attacker is now authenticated to your cloud account". AWS's IMDSv2 defends against exactly this by requiring a session token obtained via a PUT, which a naive fetcher will not send; Azure and GCP require a specific header for the same reason. All of which only helps if the old version is actually turned off.

4. Allowlist bypasses, which is where most of the real work is

Most teams that know about SSRF have written a check. Most of those checks validate the URL string, then hand the original string to the HTTP client — and the gap between what the validator parsed and what the client eventually connected to is where the bug lives. The recurring causes: redirects followed after validation, DNS entries that resolve to an internal address, alternative encodings of the same address, URL parsers disagreeing about where the host ends, and names that resolve differently between the check and the connection.

5. The flat internal network behind it

SSRF's severity is decided by what is reachable once you have it. In a segmented environment with authenticated internal services, an attacker gets a slow, blind port scanner. In the more common environment — internal admin panels with no authentication because "it's internal", databases and caches listening without credentials, a service mesh that trusts source IP — the same bug reaches everything. We assess SSRF against your internal surface, not a generic one, because the identical flaw is a Low in one estate and a Critical in another.

Blind SSRF is routinely dismissed because "it returns nothing". That reasoning is backwards: no response body means no evidence in the application's output, not no request. We confirm it out-of-band — a request that leaves your infrastructure and arrives at a host we control proves reachability outright, and the source address tells us where it came from. A bug nobody can see is still a bug an attacker can use.

How we test for it

  1. Inventory every input that could become a destination. Not just fields called url — webhook targets, callback addresses, identity-provider metadata URLs, S3 or bucket import paths, PDF and image sources, anything that takes a hostname, and any file upload whose contents get parsed by something that can make requests.
  2. Prove reachability out-of-band first. We point the feature at a host we control and watch for the DNS lookup and the HTTP request. This confirms the vulnerability without touching anything of yours, and the source address tells us which egress path the request took.
  3. Separate blind from non-blind. If the response body comes back to the user, the impact is immediate and we demonstrate it. If it does not, timing, status codes and response-size differences still map what is reachable.
  4. Test the allowlist the way an attacker would. Redirects, alternative address encodings, parser disagreements, hostnames that resolve inward. The question is never "is there a check" but "does the check see the same destination the HTTP client eventually connects to".
  5. Go for the metadata endpoint — then stop and ask. Demonstrating that a request reaches the metadata service is almost always sufficient proof of critical impact. Retrieving live cloud credentials is a materially bigger action, so it happens only with your explicit written agreement, under the rules of engagement, and never as a surprise in the report.
  6. Map the blast radius. What else answers from that position? This is what moves a finding's severity, and it is the part a scanner cannot do because it requires knowing what your internal services are worth.

Everything is written up with the request, the account, the out-of-band evidence and the reachable surface, so your team can reproduce it rather than take our word for it — the standard we described in what's in a penetration test report.

Six things to check in your own application this week

  • Enforce IMDSv2 on every instance and switch IMDSv1 off, or apply the equivalent required-header protection on Azure and GCP. This single change downgrades most cloud SSRF from critical to noisy.
  • Grep for your HTTP client's constructor. Every call site that builds a request from anything a user influenced is a candidate — and the list is usually longer than the team expects.
  • Validate the resolved address, not the string, and re-validate after every redirect. Better still, stop following redirects in fetchers that do not need to.
  • Route outbound fetches through a dedicated egress proxy in its own network segment, with no cloud role attached and no route to internal services. Then an SSRF reaches the internet and nothing else.
  • Do not return fetched content to the user verbatim. If the feature only needs to validate a webhook, return success or failure — not the body.
  • Turn off external entity resolution in every XML parser, and treat uploaded documents that get rendered as network-capable input, because that is what they are.

If you would like someone to go looking properly — including the webhook field nobody remembers shipping — that is the work we do: fixed-price manual web application testing, with the retest included.

Build your quote

What can your server reach that your users can't?

That is the question an SSRF test answers. Fixed price, manual testing, retest included.