In the previous lesson, you built solid defenses using manual IP range checking. Those defenses work against basic attacks, but determined attackers have tricks to bypass hand-written validation. Let's see how, and then upgrade to library-based validation that handles these edge cases automatically.
A blocklist that only checks for exact string matches like localhost or 127.0.0.1 can be defeated using alternative IP representations, URL encoding, redirect chains, and DNS rebinding techniques. In this lesson, you will learn how attackers bypass weak defenses and how to build validation that is truly robust using proper IP parsing libraries. By understanding both the attack and defense perspectives, you will be able to protect your applications against sophisticated SSRF exploitation.
Let's look at a typical weak defense that many developers implement when they first learn about SSRF. This code appears to address the vulnerability by blocking dangerous hostnames, but its approach is fundamentally flawed because it relies on simple string matching.
The code starts correctly by parsing the user-provided url using the built-in URL class. This ensures we are working with a well-formed URL structure rather than a raw string. However, the protection mechanism that follows is where things go wrong.
This blocklist checks if parsedUrl.hostname exactly matches either localhost or 127.0.0.1. The problem is that IP addresses and hostnames have many alternative representations that resolve to the same destination.
The code assumes that blocking two specific strings is enough, but IP addressing is far more flexible than that. An attacker can represent 127.0.0.1 in hexadecimal, decimal, octal, or even shortened forms, and all of these will resolve to the same loopback address while bypassing this string-based check.
Once the weak validation passes, the code makes the HTTP request using axios.get(url). The comment hints at the problem: there are numerous representations that will bypass the blocklist but still reach localhost. This implementation gives developers a false sense of security while leaving the application completely vulnerable to SSRF attacks.
IP addresses can be written in many different formats, and operating systems will parse and resolve all of them correctly. Attackers exploit this flexibility to bypass blocklists that only check for the standard dotted-decimal notation. Let's explore the most common alternative representations and see how they bypass our weak defense.
The shortened notation takes advantage of the fact that you can omit trailing octets, and they will be assumed to be zero. Instead of writing 127.0.0.1, you can simply write 127.1, where the .1 represents the last octet and the missing middle octets are filled with zeros. Here's how an attacker would exploit this:
This request completely bypasses the blocklist because parsedUrl.hostname will be 127.1, which does not match the string 127.0.0.1 in our blocked array. However, when axios makes the request, the operating system resolves 127.1 to 127.0.0.1 and successfully reaches localhost.
The hexadecimal representation expresses each octet in base-16 notation, prefixed with 0x. The IP 127.0.0.1 becomes 0x7f.0.0.1, where 0x7f is hexadecimal for 127. You can also represent the entire IP as a single hexadecimal number: 0x7f000001. Both formats work and bypass string-based blocklists:
Again, the hostname 0x7f000001 does not match our blocked strings, but the system resolves it correctly to 127.0.0.1 and reaches the internal admin endpoint.
Even more obscure is the decimal representation, where you convert the entire IP address to a single decimal number. Each octet contributes to this number based on its position: 127 * 256³ + 0 * 256² + 0 * 256¹ + 1 * 256⁰ = 2130706433. This means 2130706433 is a valid way to reference 127.0.0.1:
This technique is particularly sneaky because the URL looks nothing like an IP address. A developer reviewing logs might not even recognize that 2130706433 is targeting localhost.
Finally, the octal representation uses base-8 notation, indicated by a leading zero. The IP 127.0.0.1 can be written as 0177.0.0.1, where 0177 in octal equals 127 in decimal:
All four of these alternative representations bypass our blocklist while reaching exactly the same destination. This demonstrates why string-based validation is insufficient for IP addresses. We need to parse and normalize the IP address into a standard form before checking if it is in a blocked range.
Beyond alternative IPv4 representations, attackers can exploit IPv6 addresses and URL encoding tricks to bypass weak validation. These techniques are particularly effective against defenses that only consider IPv4 or do not properly decode URLs before validation.
IPv6 localhost is represented as ::1, which is the compressed form of 0000:0000:0000:0000:0000:0000:0000:0001. If your blocklist only contains IPv4 addresses like 127.0.0.1, an attacker can simply use the IPv6 equivalent:
Notice the square brackets around ::1. In URLs, IPv6 addresses must be wrapped in brackets to distinguish the colons in the address from the colon that separates the hostname from the port. When this request is processed, parsedUrl.hostname will be ::1, which does not match our IPv4-only blocklist. However, the request successfully reaches localhost because ::1 is the IPv6 loopback address.
The URL encoding bypass exploits the fact that some URL parsers normalize encoded characters while others do not. A particularly dangerous technique is null byte injection, where an attacker adds %00 (the URL-encoded null byte) to manipulate hostname parsing:
This attack works if the validation logic decodes the URL differently than the HTTP client making the request. Some parsers might see the hostname as 127.0.0.1%00.attacker.com and treat it as an external domain that passes the blocklist. However, when the HTTP client processes the URL, it might stop at the null byte, treating the hostname as just 127.0.0.1 and making a request to localhost.
The success of this technique depends on the specific behavior of the URL parser and HTTP client being used. Modern libraries have generally fixed these inconsistencies, but the attack vector demonstrates an important principle: validation and execution must use the same parsing logic. If they interpret the URL differently, attackers can exploit the gap between them.
Even if you successfully validate that a URL points to an external, safe destination, attackers can use HTTP redirects to bypass your checks. The idea is simple: the attacker hosts a server that passes validation but immediately redirects to an internal target. Your validation happens before the redirect, but the HTTP client follows the redirect and reaches the forbidden destination.
Let's say the attacker controls attacker.com and sets up a redirect endpoint that sends visitors to the AWS metadata service. The attack looks like this:
When your server receives this request, it validates attacker.com, which is a perfectly legitimate external domain. The validation passes because there is nothing suspicious about the hostname. Your server then makes a GET request to http://attacker.com/redirect?target=http://169.254.169.254/latest/meta-data/.
The attacker's server responds with an HTTP 302 redirect that looks like this:
By default, most HTTP clients automatically follow redirects. Your axios client receives this redirect response and immediately makes a second request to http://169.254.169.254/latest/meta-data/, which is the AWS metadata endpoint. The attacker has successfully bypassed your validation by introducing a redirect chain.
This attack works because validation and execution are separated in time. You validate the URL once, but the HTTP client might make multiple requests if redirects are involved. The solution is to disable automatic redirect following by setting maxRedirects: 0 in your HTTP client configuration. This ensures that the URL you validated is exactly the URL your client fetches, with no opportunity for redirection shenanigans.
You might also encounter multi-hop redirect chains where the attacker uses several intermediate redirects to evade detection. For example, attacker.com might redirect to trusted-cdn.com, which then redirects to 169.254.169.254. Each individual redirect might look innocent, but the chain ultimately leads to an internal target. Disabling redirects entirely eliminates this entire class of attacks.
DNS rebinding is one of the most sophisticated SSRF bypass techniques, and it defeats validation that happens before the request is made. The attack exploits the fact that DNS responses can change over time, allowing an attacker to pass validation with a safe IP address and then switch to an internal IP address when the actual request is made.
Here's how the attack works at a high level. The attacker controls a DNS server and sets up a domain like rebind.attacker.com with an extremely short Time-To-Live (TTL) value, often just one second. When your server first resolves this domain during validation, the attacker's DNS server responds with a safe, public IP address like 1.2.3.4. Your validation checks this IP, determines it is not in a private range, and allows the request to proceed.
However, between the validation and the actual HTTP request (which might only be milliseconds later), the DNS record expires due to the short TTL. When your HTTP client resolves rebind.attacker.com again to make the request, the attacker's DNS server now responds with 127.0.0.1 or another internal IP address. The HTTP client makes the request to this internal IP, bypassing all your validation.
Here's what the attack looks like from the attacker's perspective:
The attacker needs to carefully time the DNS responses. Tools and services exist specifically for performing DNS rebinding attacks, handling the timing and DNS server configuration automatically. Some attackers even use services that alternate between safe and malicious IPs with each DNS query, increasing the chances that the timing works in their favor.
Why is this so difficult to defend against? The fundamental problem is the time gap between validation and execution. Even if you validate the IP address perfectly, that validation is only valid for the moment you performed it. By the time you make the HTTP request, the DNS resolution might return a completely different IP address. The only reliable defense is to perform validation immediately before making the request and ensure no DNS resolution happens after validation, which requires careful implementation.
Additionally, DNS rebinding can bypass IP-based rate limiting, logging, and other security controls that rely on consistent DNS resolution. This makes it a powerful technique for evading detection even after an attack is discovered.
Now that you understand how attackers bypass weak defenses, let's extract our validation logic into a reusable utility and upgrade to ipaddr.js, which handles all the alternative representations we just learned about. The key is using a proper IP parsing library that normalizes all representations into a standard form and checks IP ranges correctly.
First, let's create a utility module for URL validation. We need to import the necessary libraries and define our validation function:
We start with the same URL parsing as before, rejecting any malformed URLs immediately. This prevents injection attacks and ensures we are working with a properly structured URL.
Next, we validate the protocol to ensure only http: and https: are allowed:
This prevents file protocol attacks and other exotic protocols that could be abused. Now comes the critical part: resolving the hostname to its actual IP addresses and validating each one:
We use dns.resolve4() to perform a DNS lookup and obtain all IPv4 addresses to which the hostname resolves. If DNS resolution fails (perhaps because the hostname does not exist or the DNS server is unreachable), we reject the request. This is a fail-secure approach where we block suspicious or broken requests rather than allowing them through.
Note that we are only resolving IPv4 addresses here. For production systems, you should also resolve IPv6 addresses using dns.resolve6() and validate both sets of addresses. For this lesson, we will focus on IPv4 to keep the examples clear.
Now we validate each resolved IP address using ipaddr.js:
The ipaddr.parse() function takes an IP address string and returns an object representing that address. It automatically handles all alternative representations we discussed earlier: hexadecimal, decimal, octal, and shortened forms are all normalized into a standard format before the range is checked.
The range() method returns a string describing what type of address this is. The possible values include unicast (normal public internet addresses), private (RFC 1918 private networks), loopback (127.0.0.0/8), linkLocal (169.254.0.0/16, which includes cloud metadata), and several others. By checking range !== 'unicast', we reject everything except normal public addresses.
This single check blocks all the bypass techniques we covered: 127.1 gets normalized to 127.0.0.1 and identified as loopback; 0x7f000001 gets normalized and identified as loopback; ::1 gets identified as IPv6 loopback (if we were checking IPv6); and so on. The library handles all the complexity of IP address parsing, so we do not need to manually check individual ranges.
Let's integrate this validation into our endpoint:
We call our validateExternalUrl function and reject the request if it returns false. This happens before we make any HTTP requests, ensuring that only safe, validated URLs proceed.
Finally, we make the request with additional safeguards:
Setting maxRedirects: 0 prevents redirect chain attacks by refusing to follow any redirects at all. Setting timeout: 5000 prevents denial-of-service attacks where the attacker provides a URL that never responds, tying up your server's connection pool.
Does this defend against DNS rebinding? Partially. The validation happens immediately before the request, minimizing the time window for DNS rebinding. However, there is still a small gap between validateExternalUrl() (which performs DNS resolution) and axios.get() (which performs its own DNS resolution). For maximum security against DNS rebinding, you would need to pass the resolved IP addresses directly to the HTTP client or implement caching that ensures both validation and execution use the same DNS result.
You have learned that simple blocklists are easily bypassed using alternative IP representations, IPv6 addresses, URL encoding, redirect chains, and DNS rebinding techniques. Attackers can represent 127.0.0.1 as 127.1, 0x7f000001, or 2130706433 to evade string-based checks.
The defense requires proper IP parsing libraries like ipaddr.js that normalize all representations and check IP ranges correctly, combined with DNS resolution to validate the actual destination addresses. You also learned to disable redirects and set timeouts to prevent additional attack vectors.
In the upcoming practice exercises, you will exploit weak SSRF defenses using these bypass techniques and then implement robust validation using the patterns covered in this lesson.
