Server-Side Request Forgery (SSRF) vulnerabilities allow an attacker to cause a server application to perform an unintended request. When exploited, the server could leak sensitive internal information or perform dangerous actions. Because this vulnerability depends on the capabilities of the server application, the potential impact of an attack can vary.
Webhooks are among the most common features that introduce SSRF vulnerabilities to applications. They combine arbitrary user input (the webhook URL) with the ability to make requests from the backend. It’s important to consider this threat when building and operating webhook systems.
The attack
For the purposes of this post, imagine we have a web application that is able to perform outbound requests to a user-configured endpoint.
Cloud credential leak
Every AWS compute resource has a special internal service, called the Instance Metadata Service (IMDS). This endpoint provides network information, initialization scripts, and even temporary AWS access keys. All of this is conveniently hosted at http://169.254.169.254
. The feature is great for avoiding hard-coded keys but comes at a cost—by default, it is available to every process on the server.
The scenario for exploiting this is simple: the user provides a URL of http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
to the application, then look at the response. It should look something like this:
{
"Code": "Success",
"LastUpdated": "2012-04-26T16:39:16Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "token",
"Expiration": "2017-05-17T15:09:54Z"
}
With the key in hand, the attacker can perform any action that the compute instance is permitted to do. Because AWS IAM can be complicated, some organizations provide more permission than is the server needs—using *
wildcards. Occasionally, the same IAM role will be used across staging and production environments.
Even if the server isn’t hosted in AWS, there are similar endpoints in all major cloud providers. This includes Google Cloud, Azure, and DigitalOcean.
Internal endpoints
Mature organizations might rely on private networking to protect internal resources, asserting that a VPN connection is required for access. Unfortunately, web servers tend to sit on the same network.
Imagine there’s an intranet site containing trade secrets sitting on the internal network, available for all employees to access. By convincing an application server to perform requests against the site, an attacker can steal all of those secrets.
Filesystem access
Web application servers tend to contain secrets of their own: source code, configuration files, and user uploads. An attacker providing a request URL with the file://
scheme may be able to access any file visible to the web server process. This might already be locked down depending on the library being used to perform requests from the server.
Mitigating the vulnerability
Deliver, the webhook monitoring system I’ve been working on, is written in Elixir. Naturally, the examples will also use Elixir. The same mitigations will likely be available in any technology, but specific package recommendations may not apply.
Preventing non-HTTP(S) URIs
The following example shows how the URI.parse/1
method breaks a URI into a struct.
iex> URI.parse("https://elixir-lang.org/")
%URI{
authority: "elixir-lang.org",
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}
A first pass at SSRF protection might look like this:
def permit_uri?(uri) do
URI.parse(uri).scheme in ["http", "https"]
end
By locking out unacceptable schemes, we ensure that the filesystem can’t directly be accessed.
Preventing the instance metadata IP address
Another layer of protection that could be valuable is excluding 169.254.169.254
, as follows:
def permit_uri?(uri) do
parsed_uri = URI.parse(uri)
parsed_uri.scheme in ["http", "https"] and
parsed_uri.host != "169.254.169.254"
end
One interesting problem with this solution is that public DNS endpoints can return any IP address they’d like in response to an A record query. For example, this would be completely legal in DNS:
> nslookup malicious.example.com
Server: 1.1.1.1
Address: 1.1.1.1#53
Non-authoritative answer:
Name: malicious.example.com
Address: 169.254.169.254
If an attacker arranged for a request to http://malicious.example.com/latest/meta-data/iam/security-credentials/role-name
, it would be treated as though the IMDS IP address were provided to begin with.
Looking up DNS hostnames
Erlang’s built-in :inet_dns
module can be used directly in order to perform DNS lookups. On the Erlang side, IPv4 addresses are expressed as tuples with four elements, and strings used for networking have to be of type charlist
instead of binary
. Clearly, this implementation is getting complicated.
defmodule SSRFProtection do
@disallowed_hosts [
{169, 254, 169, 254}
]
defp resolve_host(host) do
host = to_charlist(host)
case :inet.parse_address(host) do
{:ok, addr} ->
[addr]
_ ->
:inet_res.lookup(host, :in, :a)
end
end
def permit_uri?(uri) do
parsed_uri = URI.parse(uri)
parsed_uri.scheme in ["http", "https"] and
not Enum.any?(
resolve_host(parsed_uri.host),
&(&1 in @disallowed_hosts)
)
end
end
By running the user-provided URIs through the permit_uri?/1
method of SSRFProtection
, we can prevent this class of requests.
iex> SSRFProtection.permit_uri?("https://google.com/something")
true
iex> SSRFProtection.permit_uri?("https://169.254.169.254/something")
false
iex> SSRFProtection.permit_uri?("https://malicious.example.com/something")
false
iex> SSRFProtection.permit_uri?("file://C:\\inetpub\\wwwroot\\secret.html")
false
Perform validation on every request
While there is some level of protection provided by validating a user-provided URI when it’s first submitted, it’s not the best we can do. DNS is not static. An attacker could submit a URI that previously resolved to 123.123.123.123
, but update the record to 169.254.169.254
after the application validates it.
This is why it’s important to run permit_uri?/1
(or its equivalent) for each request the server makes.
Using SafeURL
As indicated above, this implementation is messy and not great. It describes the concepts, but it’s probably not the most production-ready solution. Luckily, there’s an open-source Elixir package called SafeURL that encapsulates SSRF-protecting validation into a single module.
Wherever SSRFProtection.permit_uri?/1
was used, we can instead use SafeURL.allowed?/1
. This library has a built-in list of IP addresses, ranges, and schemes that are a great default. Depending on your use case, it can even perform GET
requests for your application through an optional HTTPoison
integration.
Similar libraries
There are similar libraries for SSRF protection in most programming languages. A couple that appear to be reputable are:
- Ruby:
ssrf_filter
- Python:
advocate
Taking it further
Don’t follow redirects
Some request libraries automatically follow HTTP 301 and 302 redirects. Ensure this behavior is disabled when performing requests against user-provided URLs. A malicious actor could host https://happy-looking-site.com
, where it redirects to 169.254.169.254
. If the web application follows the redirect, the attack will succeed.
Security groups
Make sure to configure security groups or the cloud firewall for the hosting provider of the web application in a way that limits access to what’s strictly necessary. If it doesn’t make sense for the application to reach a specific internal service, don’t allow it.
Proxy
An additional layer of security that some companies choose to implement is an HTTP egress proxy. These typically are instances of Squid, Envoy, or a cloud provider hosted solution. When outbound requests are made by the web application, they go through the proxy. This proxy should be configured with no permission to reach back into the private network and ideally will prevent requests to bad IPs.
Wrapping up
Security is tricky to get right and can have disastrous side-effects when done incorrectly. Attackers will poke holes in every line of defense, which is why it’s important to consider your threat model and establish a defense-in-depth strategy.
While writing a solution in-house can seem reasonable, there are tricky edge cases that might be solved in a well-accepted open-source package. Look to the community for the best defense.
Go and protect your application from SSRF vulnerabilities!