SSRF (Server Side Request Forgery) is a vulnerability used by attackers to do network internal requests when an URL is passed as an argument of your functions.
The impact of this vulnerability is completely related to what's accessible inside your internal network and what's the purpose of your function working on the URL attacker controlled argument.
Let's explain it with a Slite vulnerability discovered during a penetration testing.
Import content process
Slite gives the ability to import content from various format into a note.
To lighten our media upload process, we directly upload files from browser into google cloud bucket.
Authenticated browser asks via GraphQL
getFileUploadUrl a signed unique upload URL.
Then it uses it to upload the file and then call
importFilesFromUrlList with the read URL as an argument.
The backend then download file from this URL and try to convert it to a note.
As you may guess, if you specify internal URL into
importFilesFromUrlList you'll exploit the SSRF and create a note from internal endpoint.
SSRF exploitation is not trivial as you should know internal mapping
In our case someone could call our internal JobManager and bypass Basic Authentication done by external reverse proxy.
In this specific case, the solution is quite simple as we know the URL should be from our own specific google bucket. We added an allowlist check on
importFilesFromUrlList to be sure it's our own bucket and even use google storage API instead of simple HTTP download.
Universal solution ?
OK, we understood what is an SSRF but our solution was very case specific. Most of the time you can't pass an allowlist of known external URL you will accept.
Blocklisting internal URL is fastidious and error prone. Moreover you can't imagine how many bypass techniques there are if you play with string comparison.
IPV6 ? IPV4 ? DNS ? Punycode ? decimal ipv4 ? Pick your poison...
Network level firewalling
As we could see, URL string to network request could be fooled by many techniques.
What's harder to fool then is network level communication itself.
If we could make all our dangerous requests from a single node and firewall it, it should fix the root problem.
A well known technique to centralize requests in one place is to use a proxy SOCKS.
We picked Dante proxy as it seems to be the most robust and efficient proxy SOCKS5 implementation.
Then we can:
This is tedious and we all know someone who lock itself outside of the host...
Furthermore, it's really not compatible with kubernetes and micro services contenerized architecture sharing the same host...
Setup this proxy SOCKS on a different isolated network
A trade-off between practicality and security, why not ?
We will need to communicate with this proxy SOCKS using external connection so let's use SOCKS5 authentication.
Since the request carries the password in cleartext, this subnegotiation is not recommended for environments where "sniffing" is possible and practical.
Let's wrap it with TLS then !
Use Dante configuration
Dante provide a simple block and allow configuration which will operate at the network level after SOCKS5 negotiation.
This way we can integrate it in our private network, communicate with it but it will prevent further communication to go back into our private network.
Let's use it in our code
At Slite we use NodeJS with Typescript and we use got to perform our HTTP requests.
We started to add a custom
This rule looks for
got function calls second argument.
The second argument in
got is the HTTP request options.
We want our developers to explicitly wrap this options with
This way, everytime we use
eslint will force us to think about the implication of this HTTP request.
Is it inter service communication ? → use
Is it user controlled URL ? → use
Wrapping options ?
What those two functions are doing ? You should ask yourself...
The unprotected function does nothing and the protected function injects an HTTP agent to enforce usage or our SOCKS5 proxy.
That's it ! We could do the same with axios or any other HTTP client library.
Triple check the library you use doesn't support other schema than
https? as it will introduce nastier vulnerabilities (file://path/to/your/code...)!
Agents are only used for HTTP and HTTPS requests.
We created a docker image wrapping all together: https://github.com/sliteteam/anti-ssrf-proxy
You can double check the block list
If you want to protect Dante SOCKS5 authentication with TLS, you can mount TLS certificate and private key and add the environment variable
It will bind Dante to
localhost:1081 and use Stunnel4 to wrap it via
socks-proxy-agent nodejs library doesn't support TLS wrapped SOCKS5, we've made a PR and a fork
@slite/socks-proxy-agent to introduce
tls option in the library.
For our fellow bounty hunter out there
We've made an internal route to help you identify SSRF:
If one of our pod do a request on
We will receive a Sqreen alert and the attacker will receive a proof token to send us with its exploitation report.