~ 3 min read
An Introduction to SSRF Bypasses and Denylist Failures

I recently disclosed several vulnerabilities (CVE) that exposed critical SSRF bypasses in popular npm packages. What can you learn from the mistakes those maintainers made? what are some weak code patterns that introduce an SSRF? Let’s dive in.
What’s SSRF? In short, A server-side request forgery (SSRF) vulnerability happens when the Node.js application makes unintended server requests from the Node.js backend to other servers (internal or external), based on user-controlled input. Attackers can then exploit this behavior to trick the vulnerable Node.js application into fetching all kinds of server resources and potentially accessing unauthorized actions which don’t need authentication (think about internal servers, sidecars, etc).
SSRF Denylists
Denylists are often frowned upon in the security domain because they are too brittle and can be easily bypassed or made obsolete with new attack vectors (and trust that an adversarial will not stop looking for them).
Let’s see some use-cases that denylists fail miserably with protecting against SSRF attacks:
Deny the 127.0.0.0/8 subnet
The IP address 127.0.0.1
is often used as a localhost address. Technically speaking, the entire IP address range comprising of the address space between 127.0.0.1 to 127.255.255.255 is reserved for the loopback (the localhost) address.
As such, it might seem like a good idea to check if the hostname simply begins with a ‘127’ character set such as the following code snippet:
if (hostname.startsWith('127')) { return 'Private';}
However, what if a hostname starts with the number ‘127’? For example, users are free to buy domain names such as 127-my-local-domain.com
. In this case, the above code snippet would incorrectly classify the domain provided by the user as a private IP address / hostname.
Deny the 172.16 address range
A common localhost reserved address space is 172.16.0.0
. Details matter though. While we see a lot of IP addresses starting with the string 172.16.
the actual address space is not a limited to that of all the addresses within the /16
address space. Meaning, the “last” address in that space ends at 172.16.255.255
? Not really.
The actual address space for loopback IP address spaces is limited to the scope of the following subnet definition: 172.16.0.0/21
which yields addresses starting from 172.16.0.0
and all the way up to 172.31.255.255
. This makes it a bit more tricky to check for an IP address within the range as it involves a little bit more string parsing and manipulation if you want to keep things simple and not use binary conversions and other mathematical ways to approach the problem.
👋 Just a quick break
I'm Liran Tal and I'm the author of the newest series of expert Node.js Secure Coding books. Check it out and level up your JavaScript
Common SSRF Bypasses
- resolve localtest.me which resolves to 127.0.0.1
- not including the multicast address space (
224.0.0.0/4
andff00::/8
) which allows for this:
'http://239.255.255.250:1900' // Common SSDP discovery address
- hex encoded 127.0.0.1 such as:
console.log(ip.isPublic("0x7f.1"));
- variations such as:
127.1, 127.0.1, 127.00.0x1, 127.0.0x0.101200034567012.1.2.3fe80::0001, 000:0:0000::01, 000:0:0000:0:000:0:00:001::fFFf:127.0.0.1
So, ultimately, do not allow redirects in HTTP requests.
For example, you approve some-domain.com
which resolves to public IP but the GET request ends up with a redirect to a local IP address that isn’t checked because only the initial domain was checked. Solve this by disallowing redirects follow or by including SSRF at each socket connection made by the HTTP layer.