Correctly Validating IP Addresses: Why encoding matters for input validation.
Recently, a number of libraries suffered from a very similar security flaw: IP addresses expressed in octal were not correctly interpreted. The result was that an attacker was able to bypass input validation rules that restricted IP addresses to specific subnets.
The vulnerability was documented in (this list is unlikely to be complete):
- Node.js netmask package [1]
- Perl (various packages) [2]
- Python stdlib ipaddress module [3]
All of these vulnerabilities were caused by a similar problem: These libraries attempted to parse IP addresses as a string. Later, standard-based "socket" libraries were used to establish the actual connection. The socket libraries have their own "inet_aton" function to convert an IP address string to a long unsigned integer.
It isn't an accident that security researchers started to look at these libraries right now. More and more of our applications have surrendered data to cloud services and implement APIs to access the data. Server-Side Request Forgery (SSRF) is becoming more of an issue as a result. And to restrict which IP address a particular application may connect to, we often use the libraries above.
As a simple test string, use "010.010.010.010". The leading "0" indicates that the address is octal. In "dotted decimal," the address translates to Google's name server 8.8.8.8.
So if you have to validate an IP address: How to do it? First of all: Try to stick to one of the (hopefully now fixed) standard libraries. But in general, it can be helpful to stick to the same library for input validation and to establish the connection. This way, the IP address is not interpreted differently. There is also an argument to be made to just not allow octal. I leave that decision up to you. Standards do allow that notation.
For example, most languages under the hood rely on the C "Sockets" library to establish connections [4]. This library also includes functions to convert IP addresses expressed as a string into an unsigned 32-bit integer (inet_aton) and back (inet_ntoa) [5]. Take a look if your language is implementing these functions. Part of the problem of the node.js vulnerability was that node.js implemented its own "ip2long" function.
So how do you verify if an IP address is inside a subnet? Well, there is a reason netmasks are called netmasks: They are bitmasks. Bitmasking is a very efficient and simple way to verify IP addresses.
First of all: Is this IP address a valid IPv4 address?
My favorite "trick" is to convert the address to an integer with inet_aton. Next, convert it back to a string with inet_ntoa. If the string didn't change: You got a valid address. This will work nicely if you only allow dotted decimal notation. If not: just convert the string with inet_aton and keep it an integer. Not all languages may use the Sockets library for "inet_aton." Some may use their own (buggy?) implementation. So best to check quickly:
010.010.010.010 = 8.8.8.8 = 134744072 .
If you get 168430090, then you got it wrong (that would be 10.10.10.10). MySQL/MariaDB, for example, will get you the wrong answer:
MariaDB [(none)]> select inet_aton('10.10.10.10')-inet_aton('010.010.010.010'); +-------------------------------------------------------+ | inet_aton('10.10.10.10')-inet_aton('010.010.010.010') | +-------------------------------------------------------+ | 0 | +-------------------------------------------------------+
Output encoding matters for proper input validation!
So now, how do we verify if IP address "A" is in subnet B/Z ?
"z" is the number of bits that need to be the same. So let's create a mask:
mask = 2^32-2^(32-Z)
next, let's do a bitwise "AND" before comparing the result:
If ( A & mask == B & mask ) { IP Address A is in B/Z }
So in short:
- Try to use the same library to validate the IP address and to establish connections. That is probably the easiest way to avoid issues. Sadly, modern languages add additional layers, and it isn't always clear what happens in the background.
- Test! inet_aton('010.010.010.010') != inet_aton('10.10.10.10').
- IPs are long integers, not strings.
So why do I sometimes see IP addresses like "010.123.123.123" on isc.sans.edu?
Remember, we started collecting data 20 years ago. I have learned since :) But some of my early sins still haunt me. Over the last few years, we moved to store the IP addresses as 32-bit unsigned integers, but "back in the day," we used 15 digit strings and zero-padded them for easy sorting... sorry. should be mostly gone by now and if you find a case where this is still happening, let me know.
What about IPv6?
That will be a different diary :)
What about hex notation?
There are a number of additional formats that can be used for IP addresses. For a complete collection of different representations of IP addresses [6]. Test them. Let me know what you find :) .
[1] https://nvd.nist.gov/vuln/detail/CVE-2021-29418
[2] https://blog.urth.org/2021/03/29/security-issues-in-perl-ip-address-distros/
[3] https://nvd.nist.gov/vuln/detail/CVE-2021-29921
[4] https://www.gnu.org/software/libc/manual/html_node/Sockets.html
[5] https://www.gnu.org/software/libc/manual/html_node/Host-Address-Functions.html
[6] https://www.hacksparrow.com/networking/many-faces-of-ip-address.html
---
Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|
Application Security: Securing Web Apps, APIs, and Microservices | Online | US Eastern | Jan 27th - Feb 1st 2025 |
Comments
Anonymous
May 12th 2021
3 years ago