Mitigating Server-Side Request Forgery
Tags: security, elixir
Published: 2023-04-26 13:30:00

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)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). This endpoint provides network information, initialization scripts, and even [temporary AWS access keys](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials). 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:

```json
{
  "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](https://cloud.google.com/compute/docs/metadata/overview), [Azure](https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=windows), and [DigitalOcean](https://docs.digitalocean.com/products/droplets/how-to/retrieve-droplet-metadata/).

### 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](https://deliver.dev), 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](https://hexdocs.pm/elixir/1.12/URI.html#parse/1) breaks a URI into a struct.

```elixir
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:

```elixir
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:

```elixir
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:

```shell
> 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.

```elixir
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.

```elixir
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](https://hexdocs.pm/safeurl/SafeURL.html) 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`](https://rubygems.org/gems/ssrf_filter)
- Python: [`advocate`](https://pypi.org/project/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](https://aws.amazon.com/blogs/networking-and-content-delivery/providing-controlled-internet-access-through-centralised-proxy-servers-using-aws-fargate-and-privatelink/), [Envoy](https://blog.palantir.com/using-envoy-for-egress-traffic-8524d10b5ee2), or a cloud provider [hosted solution](https://aws.amazon.com/blogs/networking-and-content-delivery/migrating-from-squid-web-proxy-to-aws-network-firewall/). 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](https://csrc.nist.gov/glossary/term/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!