Series · 3 partsWhen Access Checks FailPart 3 · You are here
- 1The Pipeline Has No Secret. It Still Has a Cloud Identity.
- 2The Signature Is Valid. The Token Still Belongs Somewhere Else.
- 3The Request Stayed Server-Side. The Credential Did Not.You are here
SSRF in 60 seconds
An SSRF finding is usually demonstrated with a URL and reported with a sentence: “the server can make arbitrary outbound requests.” That sentence describes a capability and says almost nothing about impact.
The server was already allowed to make requests. The security question is what those requests inherit: internal routing, trusted source addresses, ambient credentials, proxy exceptions, redirect handling, and access to link-local control planes. SSRF becomes serious when an untrusted input crosses one of those boundaries and the resulting request is treated as the server rather than as the caller.
The URL is only the first edge. The route is the finding.
Four decisions happen after validation
Most application code validates a string, then hands it to a network stack that performs several additional transformations:
- The parser resolves scheme, authority, user information, host, port, and path.
- DNS converts the accepted hostname into one or more addresses.
- The HTTP client follows redirects, possibly to a new scheme or address.
- A proxy, service mesh, or host route decides where the request actually goes.
If policy runs only before those transformations, it authorizes a name and the network connects to an address. The two can disagree for the same reasons a reverse proxy and application can disagree about a path.
A blocklist for 127.0.0.1 does not express the intended policy. The intended policy is
usually “this feature may fetch public HTTPS resources from these destinations, must not
follow a redirect outside that set, and must never reach private, loopback, link-local, or
control-plane ranges after every DNS resolution.”
That is an allowlist plus a connection-time address check, not a collection of forbidden strings.
Start with a canary, not an internal target
The first test should answer whether the feature performs a server-side request and what that request looks like. Use an assessment-controlled HTTPS endpoint with a unique per-request path. Record source address, method, headers, DNS queries, redirect behavior, and timing.
https://canary.example.test/ssrf/<engagement>/<request-id>
That observation separates server-side fetches from client-side navigation and exposes hidden behavior without touching an internal service. A request arriving from the server’s egress address is positive evidence. No arrival is not automatically a negative result: the application may queue work, restrict schemes, resolve through a proxy, or fetch only certain content types. The canary should mirror the expected response shape before the test expands.
Next, change one property at a time:
- a redirect from one allowed canary hostname to a second controlled hostname;
- a hostname resolving to multiple public canary addresses;
- a slow response to identify timeout and retry behavior;
- a non-HTTPS scheme to confirm it is rejected before connection;
- an allowed hostname that redirects toward a prohibited address, which must be blocked without contacting that destination.
These tests map the client’s state machine. They do not require probing real internal hosts.
Validate after DNS and after every redirect
The application cannot decide whether a destination is public from the hostname string alone. It has to resolve the name, inspect every returned address, and bind the connection to an allowed result without letting a later lookup silently change the answer.
Redirects restart that process. A 302 is not permission to inherit the original trust
decision. The new URL needs the same scheme, host, port, DNS, and address policy as the first.
If the client changes method or forwards sensitive headers across hosts, those behaviors are
part of the finding too.
The safest fetcher is deliberately boring:
parse canonical HTTPS URL
-> compare hostname and port with explicit allowlist
-> resolve all addresses
-> reject private, loopback, link-local, multicast, and reserved results
-> connect to the validated address
-> disable redirects, or re-run the entire decision for each hop
-> enforce response-size, content-type, and time limits
An outbound proxy or service-specific fetch broker makes this easier to enforce consistently. Application code is bad at reproducing network policy in every feature that accepts a URL.
Metadata is an identity boundary, not a special URL
On an EC2 instance, the Instance Metadata Service is available through a link-local address. Applications sometimes treat blocking that literal address as the SSRF fix. The durable control is stronger: require IMDSv2, constrain metadata reachability to workloads that need it, and give the attached role only the permissions the workload requires.
IMDSv2 uses a session-oriented token. A client first sends a PUT to request a token and
then presents that token in subsequent metadata GET requests. This blocks many simple SSRF
primitives that can issue only a fixed GET or cannot set the required header. It does not
turn arbitrary server-side request capability into a safe design. A sufficiently capable
primitive, local proxy, or compromised process may still satisfy the exchange.
The hop limit is another boundary. It controls how far the metadata token response may travel, which matters when container networking adds a hop. The correct value depends on the deployment topology; setting it from a generic hardening checklist without testing the workload can either break credential delivery or leave a broader route than intended.
For an authorised validation, use a dedicated test instance with a lab role that grants no resource access. Prove only whether the application can reach an inert metadata path such as instance metadata information under the configured IMDS mode. Do not retrieve or store role credentials. The finding is stronger when it proves the boundary without creating a new credential exposure in the evidence package.
Egress decides whether one bug becomes a platform path
Application validation is the first control. Network egress is the independent negative control. A workload that needs to fetch avatars from one image service should not have a general route to every RFC1918 segment, orchestration API, metadata endpoint, and management plane.
That separation matters because URL validation code changes frequently. Egress policy can keep a parser regression from becoming an identity compromise. Conversely, a perfect application allowlist should not be used as evidence that the network boundary is safe; each control should be tested with the other assumed to fail.
Evidence matrix
| Signal | What it proves | Negative control | Defender verification |
|---|---|---|---|
| Unique canary request arrives from server-side infrastructure | The application performs an outbound request and reveals its fetch behavior | Use an unsupported scheme or disallowed hostname and require no canary contact | Correlate application request ID, DNS, proxy, and egress logs |
| Redirect to a second controlled public host is revalidated or blocked | Redirect handling does not inherit the first destination’s trust blindly | Redirect toward a prohibited test address and confirm no connection attempt leaves the host | Test every redirect hop for scheme, host, port, resolved address, method, and forwarded headers |
| All DNS answers are checked and the connection stays bound to an allowed result | Policy applies to addresses rather than only the original hostname | Return a mixed allowed/prohibited answer set and require rejection | Instrument resolver results and destination IP at connection time |
| IMDSv2-required test instance rejects a simple headerless metadata fetch | The metadata boundary resists basic GET-only SSRF | Confirm a legitimate SDK on the same workload still obtains its intended identity | Inventory HttpTokens, hop limit, metadata exposure, and attached role permissions |
| Egress policy denies private, link-local, and control-plane routes | A parser failure does not automatically become an internal pivot | Attempt an approved canary connection in each denied class without touching production services | Validate route tables, proxies, service-mesh policy, and host firewall from the workload identity |
The pattern I keep seeing
The application team fixes the input string. The network team owns egress. The cloud team owns the workload role. Each closes the piece visible from its layer, and the request path between them remains undocumented.
That is how a finding alternates between “High” and “Low” depending on who reads it. One person sees a URL fetcher. Another assumes metadata credentials. Neither has evidence for the whole chain.
What to hand the defenders
The resolved route. Record the canonical URL, every DNS answer, redirect hop, destination address, proxy path, and the exact trust boundary crossed.
Capability separated from impact. A public canary proves server-side fetching. Internal reachability proves a network boundary failure. Metadata or control-plane access must be validated independently and safely before claiming identity impact.
Two control owners. Fix the fetcher with an allowlist and post-resolution validation; fix the platform with egress restrictions, IMDSv2, and least-privilege workload identity. Either control can regress. The pair is what makes the route survivable.
The request may never leave the server. The trust it carries can cross half the environment.
How current is this note?
The latest source-review, content-update, or publication date is shown.
The author completed a technical review. This does not, by itself, claim lab reproduction.
