Chapter overview
In Chapter 1, we looked at SSRF not simply as a problem in which a “dangerous URL” is supplied as input, but as a problem in which user-provided information causes the server to initiate communication.
An important distinction was that the string provided by the user and the communication the server ultimately performs are not the same thing.
Chapter 2 takes that idea one step further.
One reason SSRF defenses are difficult is that the conditions under which defensive logic makes a decision at one point in time do not necessarily remain the same when the final communication is executed.
The input was checked.
The destination was checked.
The request was judged to be allowed.
Even so, that decision alone may not explain every communication that follows.
This chapter organizes the reasons for this into six perspectives.
These six perspectives are not intended to be the only complete taxonomy of SSRF. They may overlap with one another. In this chapter, they are used as an analytical model for organizing recurring ways in which SSRF defenses can become difficult.
1. Why Checking the Input Alone Is Not Enough
When thinking about SSRF defenses, input validation is often the first measure that comes to mind.
A user-supplied URL or host name is checked, and the request is allowed if the destination appears acceptable.
That idea is not wrong in itself.
The problem is that multiple processing steps may exist between the point at which the input is checked and the point at which communication actually occurs.
The input string may be parsed.
A name may be resolved.
A connection candidate may be selected.
Processing may be handed off to another component.
One communication may also cause another communication to occur.
And the meaning of the same input may differ depending on the environment, privileges, and trust relationships under which that processing is executed.
For SSRF defense, therefore, it is not enough to ask only:
“What was supplied as input?”
We also need to ask:
“At what point, and on what basis, was it judged to be allowed?”
and:
“What ultimately performed the actual communication?”
2. The Decision and the Actual Communication
In this chapter, the point at which defensive logic decides that a communication is allowed will be treated as the decision point.
The point at which network communication actually occurs will be treated as the execution point.
In a simple flow, these two points may appear to be close together.
In a real application, however, the path between decision and execution may involve:
- URL parsing
- name resolution
- connection-candidate selection
- redirects
- delegation to a library
- a proxy or another component
- environment-specific name resolution or network controls
The important question is therefore not merely whether something was checked once.
It is:
how far that decision remains valid across the processing that follows.
The rest of this chapter examines that problem from six perspectives:
- Interpretation Gap
- Resolution / Time Gap
- Lifecycle / Hop Gap
- Execution-Path Gap
- Environment / Reachability Gap
- Privilege / Trust Context Gap
These are not six independent classes of vulnerability.
Several of them may overlap within the same system.
3. Interpretation Gap
The first perspective concerns a simple but important problem: the same input is not necessarily interpreted in the same way by every component that processes it.
For a URL, even a single processing path may involve different responsibilities such as:
- splitting a string into components
- extracting the scheme
- extracting the host
- normalization
- handling syntactic errors
- deciding whether the application should allow the request
These responsibilities are not equivalent.
For example, Python's urllib.parse provides interfaces for parsing and decomposing URLs, but that parsing operation does not itself decide whether a destination should be permitted for security purposes.
Similarly, a validation error defined by a URL specification is not necessarily the same thing as the application's own accept/reject decision.
The key point is:
being able to parse an input is not the same as being able to conclude that it is safe.
Normalization rules may also differ across components of a URI such as the scheme, host, and other fields.
For that reason, defensive logic needs to consider:
which component interprets which representation, and according to which rules.
This chapter does not claim that two parsers were observed to produce different interpretations in a particular deployed system.
The point here is the structural reason why differences in interpretation can arise between components.
4. Resolution / Time Gap
The next perspective is that the destination considered at the time of a decision is not necessarily identical to the destination selected when the connection is actually made.
When a host name is involved, the application is not dealing only with a string.
At connection time, the name may be resolved into one or more candidate addresses, and one of those candidates may be selected for the actual connection.
Some APIs, such as Python's create_connection, document behavior in which multiple candidate addresses are obtained for a name and connection attempts may be made against those candidates.
In specific versions and direct-connection paths, urllib3 can likewise be traced to code that attempts socket connections against address candidates returned by the system.
The important distinction is:
“the host name was checked” and “this address was selected for the connection” are not conceptually the same operation.
Time also matters.
DNS records have TTL values, but it would be an oversimplification to assume that every resolver must always discard cached data at the exact instant a TTL expires.
Later standards and implementation conditions may permit additional behavior, including the use of stale data under defined conditions.
The bounded conclusion for this chapter is therefore:
name resolution, candidate selection, and time can introduce state between the point of decision and the point of connection.
This does not mean that a destination will always change, or that every request will always trigger a new resolution.
5. Lifecycle / Hop Gap
Communication does not always end after a single request.
One request may cause another request to occur.
A redirect is one common example.
When a client follows a redirect, a subsequent request is made to the redirect target.
This creates another important question:
does the decision made for the original destination remain valid for the communication that follows?
Not every client automatically follows redirects.
And a redirect does not always change the HTTP method.
So the correct conclusion is not:
“redirects always cause this particular behavior.”
Instead, the useful defensive question is:
when later requests are created, is the relevant decision still being applied at each stage?
In one specific WordPress source path, a branch can be traced in which a redirect destination is checked before the subsequent request is made.
That example is useful because it shows that defensive decisions may need to be considered not only at the start of a request, but also during the communication lifecycle.
It does not establish that “WordPress is always safe.”
What was confirmed is the structure of a specific code path.
6. Execution-Path Gap
The component that checks an input and the component that ultimately performs communication do not necessarily occupy the same place in the system.
After an application makes a decision, processing may be handed off to:
- an HTTP library
- another abstraction layer
- a renderer
- a fetcher
- a proxy
- a worker
In a fixed WordPress source path, processing can be traced from the REST-side logic through the HTTP layer, Requests, and cURL, showing that the responsibilities for checking and executing communication can be distributed across multiple components.
This provides a concrete example of how:
the decision point and the execution point can exist at different locations in the code path.
However, this alone does not establish that:
“the decision was lost somewhere in the path,”
“the request was executed on a different host,”
or
“a vulnerability exists.”
What is established is that, within a specific same-process path, responsibilities are distributed across multiple components.
Some interfaces, such as renderers that can delegate external resource retrieval to another fetcher, provide another example of such separation.
But this information alone does not determine what decision was made on the application side before that fetcher was reached, or how far that decision was preserved.
Those points therefore cannot be treated as established facts here.
7. Environment / Reachability Gap
The same name can have a different meaning depending on the environment from which it is resolved.
In Kubernetes service discovery, for example, the meaning of an unqualified service name can depend on the namespace of the Pod issuing the query.
This means that the same string does not necessarily resolve to the same meaning when the execution environment changes.
That is one reason why the input string alone may be insufficient to fully understand the communication destination.
The same distinction applies to network controls.
The presence of a Kubernetes NetworkPolicy object does not by itself guarantee that traffic is being enforced according to that policy.
A network implementation that supports and applies the policy is also required.
Again, the important distinction is:
the existence of a configuration is not the same as proof that the corresponding traffic control is actually being enforced.
This chapter does not claim that a particular cluster, host, service, or destination was actually reachable.
Concrete reachability remains environment-dependent and requires separate verification.
8. Privilege / Trust Context Gap
The meaning of a communication is not determined by the destination alone.
The outcome can vary depending on:
- which identity executes the communication
- which credentials are available
- which policies apply
- which privileges have been granted
- which trust relationships exist
Boto3 documentation describes credential-provider discovery mechanisms.
AWS documentation treats request-signing authentication separately from later policy evaluation and authorization.
The important distinction is therefore:
being able to obtain credentials, being able to authenticate, and being authorized are not the same thing.
The distinction introduced in Chapter 1 also remains important here:
reachability ≠ privilege ≠ authorization ≠ trust
NIST's Zero Trust Architecture likewise does not treat network location alone as sufficient grounds for implicit trust.
When evaluating the impact of SSRF, then, it is not enough to ask only whether a destination can be reached.
We also need to consider:
under which identity, privileges, and trust context that communication is executed.
This chapter does not claim to have observed the credentials, identity, or authorization outcome of any specific environment.
It would therefore also be incorrect to generalize that server-side communication always carries credentials.
9. Why a Single Defensive Checkpoint Is Not Enough
The six perspectives discussed above do not exist only as isolated problems.
An input may be checked and then resolved.
Processing may then be handed to another component.
A redirect may create another request.
That request may then execute under a particular environment and identity.
Several conditions can therefore overlap.
For that reason, SSRF defense cannot assume that:
placing a single check at one point automatically determines the meaning of every communication that follows.
This does not mean that input validation is useless.
The important questions are:
- what is being evaluated
- which point in time or processing state is being observed
- how far the decision remains valid
- whether later communication requires another evaluation
The six perspectives in this chapter are an analytical model for reasoning about those questions.
They are not an exhaustive taxonomy.
They will not appear in the same form in every system.
Even so, the distinction between:
what was decided
and
what communication was actually performed
is an important starting point for designing SSRF defenses.
10. Toward Chapter 3
Chapter 2 has focused on:
why SSRF defense is difficult to complete with a single input check.
Between the decision and the actual communication, several perspectives may matter:
- interpretation
- time and name resolution
- communication lifecycle
- execution path
- execution environment
- identity, privileges, and trust
The next question is practical:
where should defenses be placed in a real system?
What should be checked when input is received?
What should be checked during name resolution or connection establishment?
How should redirects and secondary communication be handled?
How should responsibilities be divided across components?
How should the execution environment and identity be constrained?
Those questions cannot be answered by the conceptual model alone.
They depend on the actual application structure, libraries, network, and execution environment.
Chapter 3 will build on the distinction between the decision and the actual communication introduced in this chapter and examine where defensive controls should be placed in a real system, and how those controls should be combined.