Mitigation of SSRF Vulnerabilities
Tags: webdev, security, javascript
Published: 2021-05-04 18:33:12

Businesses rely on [Anvil](www.useanvil.com/developers) for many of their most critical processes. Being in that critical path for business software, reliability and&mdash;more importantly&mdash;security is something we take very seriously at Anvil. As part of our security practices, we undergo regular auditing, pen-testing and certification to make sure we're as good as we can be on the security front. This dedication to security has allowed us to achieve our `SOC 2 Type 2` compliance. Our SOC 2 designation recognizes the effort and policies that we take to actively guard against attacks and vulnerabilities.  In this post, I'm going to dive into one of those vulnerabilities, the Server Side Request Forgery ([SSRF][SSRF]) attack, and discuss the steps we have taken to protect ourselves and our customers.


## What is SSRF?
Before we dive deeper, let’s briefly review what an SSRF attack is. Here's a good description I found:

*"Server-side request forgery is a web security vulnerability that allows an attacker to induce the server-side application to make HTTP requests to an arbitrary domain of the attacker's choosing. In typical SSRF examples, the attacker might cause the server to make a connection back to itself, or to other web-based services within the organization's infrastructure, or to external third-party systems."* [^1]

Why would this be bad? Let’s imagine a website that performs SEO analysis of web pages for its users. This website accepts a URL from Userland. A server visits that URL and then analyzes the results and displays them back to the user. Now, imagine if that URL was something like `"http://localhost/insecure-admin-page.php"`, or `"file:///etc/passwd"`. You can see how you might quickly become [`pwned`](https://www.urbandictionary.com/define.php?term=pwned).

## Anvil's Exposure
We have several parts of the system where it's necessary for us to ingest URLs from Userland. We couldn't just stop accepting this input, so we had to take some steps to protect ourselves from SSRF.


***Disclaimer: An SSRF vulnerability is potentially quite serious for any application. In order to prevent leaking any useful information about our system in this post, and to prevent readers from thinking about a "copy pasta" from this post as a way to close their SSRF exposure, I will only provide very loose pseudo code examples, and not reference any particular libraries, packages or frameworks. This post is only meant to be an interesting resource, so please do your own homework before implementing any of the things discussed here.***

In all of our APIs, we enforce a JSON schema validation of all accepted data. In addition to checking the basic shape and types of data in a given payload, most tools will allow you to perform additional validation on the data. Here's a basic validation schema example to consider:
```js
// A payload validation schema
{
  urlFromUser: {
    type: 'string',
    format: 'validUrl',
  }
}
```

In this example, `validUrl` is a reference to a custom format that we registered with the validator. As you might have guessed, the validator will check that the value provided to `urlFromUser` is a `string`, and then pass that value to our `validUrl` function, which will return a boolean indicating if the value is acceptable or not. Here's what the `validUrl` function might look like:
```js
function validUrl (url) {
  return url && /^(http|https):\/\//.test(url)
}
```

This is a nice start, but it's pretty naive. While it makes sure that the URL is in fact an HTTP(S) resource, it doesn't check to see if that resource is `"localhost"`, `"127.0.0.1"`, or any other IPs that are considered "private". We can do better.

# Mitigation

So that we didn't have to become experts in private IPs, we enlisted the help of a library that handles all of that for us. 

Our validation function might now look like this:
```js
function validUrl (url) {
  return url
    && /^(http|https):\/\//.test(url)
    && !someIpChecker.isPrivate(url)
}
```

Nice. Now a hacker can't provide us with something like `"http://127.0.0.1/admin.php"` or we will reject the input.

But wait a minute. What if the hacker owns the domain `not-evil-i-swear.com`, and has their DNS record set to resolve to `"127.0.0.1"`? With the above validation, you would be `pwned`. This means that in order to truly know if a URL is trying to access a private IP, you have to check with DNS first to see what it resolves to.

The validation might then look something like this:
```js
function validUrl (url) {
  return url
    && /^(http|https):\/\//.test(url)
    && !someIpChecker.isPrivate(url)
    && !someIpChecker.isPrivate(dnsLookup(url))
}
```

Nice, nice. This feels pretty solid and smart.

But wait another minute. What if the provided URL is validated on its way ***into*** the system, but we don't use it right away - or we will be using it repeatedly in the ***future***? What's to stop a hacker from changing the DNS entry from something innocuous to `"127.0.0.1"` after we've run our checks and have decided to let it into the system? Answer: `pwned`.

To prevent this scenario, we had to dig into the library we use to perform our web requests with these URLs. Fortunately, this library has a hook where we can intercept the moment *after* it's resolved the DNS entry for a URL, but *before* it's actually gone out and begun connecting to it. In this hook, we check the resolved host to see if it's private, and if so, we will block the request.

Nice, nice, nice.

But wait yet another minute. What if that URL resolves to a non-private IP, so we make the initial request, but then that request results in a redirect to another IP (or `host` that resolves to an IP) that is private? If your web request library doesn't trigger your protection code in that case, too: you are `pwned`. Better make sure your web request library handles this.

## Summary
Any time a system accepts input from Userland it is putting itself at risk from potential bad actors. There are myriad examples of this: [SQL Injection][SQL], [XXS Injection][XSS], and [SSRF][SSRF] to name just a few. The tough part is that in order for your website or application to do anything interesting, oftentimes you need to accept such input. It's a calculated risk that can be mitigated to the point of acceptability by using safeguards and steps along the way. Hopefully this post provides some useful information for preventing your systems from SSRF vunerabilities!


If you have questions, please do not hesitate to contact us at:
[developers@useanvil.com](mailto:developers@useanvil.com)

[SSRF]: https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
[SQL]: https://owasp.org/www-community/attacks/SQL_Injection
[XSS]: https://owasp.org/www-community/attacks/xss/
[^1]: [https://portswigger.net/web-security/ssrf](https://portswigger.net/web-security/ssrf)