
In brief
An IPv4 address is a 32-bit number. Dotted-decimal is only the most common way to write it, and URL parsers still accept decimal, octal and hexadecimal forms with one to four parts. String comparison will not normalize those forms, which is how allowlists get bypassed.
An IPv4 address is a 32-bit number. The familiar form, 192.168.1.1, is just one way to write that number down: four decimal octets separated by dots. At least five other spellings reach the same 32 bits, and browsers, URL libraries and command-line tools still accept most of them.
That is more than a trivia question. If your application blocks an address by comparing the string a user typed, the attacker gets to choose which equivalent spelling to send. This guide covers what the notations mean, proves the behaviour with real parsers, and shows the normalisation step that closes the gap.
01An IPv4 address is one 32-bit number
Dotted-decimal notation is base-256 digits. Each of the four octets carries 8 bits, and the first octet is the most significant. Read the whole thing as a single integer and you get a value between 0 and 4294967295.
| Dotted-decimal | Integer | Hex |
|---|---|---|
| 0.0.0.0 | 0 | 0x00000000 |
| 10.0.0.1 | 167772161 | 0x0a000001 |
| 127.0.0.1 | 2130706433 | 0x7f000001 |
| 169.254.169.254 | 2852039166 | 0xa9fea9fe |
| 192.168.1.1 | 3232235777 | 0xc0a80101 |
| 255.255.255.255 | 4294967295 | 0xffffffff |
Every other notation in this article is a different encoding of one of those integers. That is the single idea that makes the whole topic predictable.
02Convert dotted-decimal to an integer by hand
The conversion is positional, exactly like decimal. Multiply each octet by 256 raised to its position from the right, then add:
192.168.1.1
192 x 256^3 = 192 x 16777216 = 3221225472
168 x 256^2 = 168 x 65536 = 11010048
1 x 256^1 = 1 x 256 = 256
1 x 256^0 = 1 x 1 = 1
----------
3232235777
For 127.0.0.1 the same arithmetic gives 2130706433, which is the number in the title. Both were verified on 2026-10-04 with Python 3:
python3 -c "import ipaddress; print(int(ipaddress.IPv4Address('192.168.1.1')),
int(ipaddress.IPv4Address('127.0.0.1')))"
The reverse direction is faster to read in hexadecimal. Split the 32-bit value into four bytes, then convert each byte to decimal: 0xc0a80101 is c0, a8, 01, 01, which is 192, 168, 1, 1.
03The legacy notations a URL parser still accepts
RFC 3986 is strict. Its grammar allows exactly four decimal octets with no leading zeros, and its authors knew that implementations were not that strict: the specification has a whole section on rare IP address formats, warning that platform routines such as gethostbyname and inet_aton accept far more shapes than the grammar describes.
The WHATWG URL standard, which browsers implement, writes down that permissiveness as an algorithm. The host parser splits the input on dots and accepts one to four parts:
- Four parts: each is one octet, left to right.
- Three parts: the first two are octets and the last becomes a 16-bit value in the final two bytes.
- Two parts: the first is an octet and the last becomes a 24-bit value in the final three bytes.
- One part: the value is the whole 32-bit address.
Each part is then parsed as a number with C-style prefixes: a leading 0x means hexadecimal, a leading 0 means octal, and anything else is decimal. A part larger than 255 is only allowed in the last position, and the last part must fit the remaining bytes.
That explains all the spellings. The single number 2130706433 is the whole address. 0x7f.1 is hexadecimal 0x7f followed by 1, filling the remaining three bytes. 017700000001 is octal for the same value. 0300.0250.0.1 is 192.168.0.1 because 0300 and 0250 are octal. None of them is a different address.
One consequence of the leading-zero rule surprises people: 010.1.1.1 is not 10.1.1.1 in a URL parser, it is 8.1.1.1. Rules in this format have been in the wild long enough that changing them would break real sites, which is why the compatibility algorithm exists.
04Verified: the same address written five ways
The outputs below come from three separate parsers, all run on 2026-10-04. Node.js 24.14.1 implements the WHATWG URL standard:
node -e "for (const s of ['http://2130706433/','http://0x7f.1/',
'http://017700000001/','http://127.1/']) console.log(s, new URL(s).hostname)"
http://2130706433/ -> 127.0.0.1
http://0x7f.1/ -> 127.0.0.1
http://017700000001/ -> 127.0.0.1
http://127.1/ -> 127.0.0.1
Python's socket.inet_aton, which mirrors the C library behaviour the RFC warned about, agrees:
socket.inet_aton('2130706433') -> 7f000001
socket.inet_aton('0x7f000001') -> 7f000001
socket.inet_aton('017700000001') -> 7f000001
socket.inet_aton('127.1') -> 7f000001
And the network layer does not care how the address was written. With a throwaway HTTP server bound to 127.0.0.1 on port 8099, every spelling returned HTTP 200:
curl http://127.0.0.1:8099/ -> HTTP 200
curl http://2130706433:8099/ -> HTTP 200
curl http://0x7f.1:8099/ -> HTTP 200
curl http://017700000001:8099/ -> HTTP 200
curl http://127.1:8099/ -> HTTP 200
The last step is the one that matters. The legacy notation is not a browser curiosity; it is a real connection to a real socket.
05Why this is a security problem
If an application decides whether a request is allowed by comparing the host string against a list, the attacker controls the encoding. A filter that blocks 127.0.0.1 and localhost does not block 2130706433, and a filter that blocks 169.254.169.254 does not block 2852039166.
RFC 3986 spells out the risk in its rare-address section: these formats can become a security concern if an application attempts to filter access based on the IP address in string literal format, and the recommendation is to convert literals to numeric form and filter on the number.
The pattern shows up in server-side request forgery defences, in webhook allowlists, in image proxy filters and in redirect validation. The blocklist is usually correct in intent and wrong in implementation, because it treats a 32-bit number as a string.
A string blocklist is not an address blocklist.
127.0.0.1, 127.1, 2130706433, 0x7f.1 and 017700000001 are five encodings of one address. So are 169.254.169.254 and 2852039166. If your filter only knows the first spelling in each pair, it does not work.
06Normalize before you filter
The fix is a canonicalisation step: turn the host into an address or an integer, then compare that value against your rules. Never compare the raw string.
Strict parsers make this easier because they reject the ambiguous forms outright. Python's ipaddress module accepts dotted-decimal and integer input but raises on the legacy spellings, and Node's net.isIP returns 0 for all of them:
| Input | Python ipaddress.ip_address | Node net.isIP |
|---|---|---|
| 127.0.0.1 | 127.0.0.1 | 4 |
| 2130706433 | ValueError | 0 |
| 0x7f.1 | ValueError | 0 |
| 017700000001 | ValueError | 0 |
| 127.1 | ValueError | 0 |
That difference is the point of using a strict parser. A value that fails the strict check is not something to pass through to inet_aton and hope for the best; it is a reason to reject the request.
For an existing codebase, the practical sequence is:
- Parse the URL with your platform's URL parser, not with string splitting.
- Take the parsed hostname, which the URL standard has already normalised to a canonical IPv4 literal.
- Convert that literal to an integer or an address object.
- Compare against your allowlist and blocklist as numbers, including the private, loopback and link-local ranges.
- Resolve and re-check after DNS, because a hostname can point at a blocked address.
Steps 4 and 5 are where most SSRF defences fail in practice. Canonicalising an address only helps if the comparison happens on the canonical form, and a check on the hostname means nothing if the request later resolves it again.
07Convert between the forms
The UrsaKit Number Base Converter is the quick way to see the relationship between notations without running code.
- Paste the integer, for example 2130706433, into the Value field.
- Set From base to 10.
- Leave To base at 16, or switch it to 2 or 8 to see the bit pattern.
- Press Convert. The result panel shows binary, octal, decimal and hexadecimal together.
- Copy the hex value and split it into pairs: 7f 00 00 01 is 127.0.0.1.
The converter is exact for integers of any size, so it will not round a 32-bit value into floating point the way a naive calculator might. When you are checking a filter instead of a value, a regex tester shows what your existing pattern actually matches, and a URL decoder resolves percent-encoding before the comparison, because %31%32%37... is yet another way to hide the same string.
| Symptom | Cause | Fix |
|---|---|---|
| Blocklist misses a loopback request | Address written as an integer or octal | Normalise to an integer before comparing |
| 010.1.1.1 does not behave like 10.1.1.1 | Leading zero means octal in URL parsing | Reject leading zeros and require four canonical octets |
| A host passes the filter, then resolves elsewhere | The filter checked the name, not the resolved address | Resolve first, then filter, and pin the connection to the checked address |
| Two logs show different hosts for one request | Each layer printed its own normalised form | Log the canonical value your filter used |
08Sources
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax, Section 3.2.2 and Section 7.4 (accessed 2026-10-04)
- WHATWG URL Standard — Host parsing and the IPv4 parser (accessed 2026-10-04)
- Python documentation — ipaddress module (accessed 2026-10-04)
Convert an IPv4 integer to hexadecimal, octal and binary, exact for integers of any size, all in the browser.
Number Base ConverterFrequently asked questions
What is IPv4 decimal notation?
It is the 32-bit integer value of an IPv4 address, written without dots. 127.0.0.1 is 2130706433, because the four octets form one number in base 256.
Why does 2130706433 open 127.0.0.1?
URL parsers support a legacy IPv4 format that accepts a single decimal number as the full 32-bit address. Browsers and command-line tools inherited it, so the host resolves to 127.0.0.1.
Why does 017700000001 mean 127.0.0.1?
The legacy format lets each part use C-style numeric prefixes: a leading 0 means octal and a leading 0x means hexadecimal. 017700000001 is the octal spelling of the same 32-bit value.
Is 010.1.1.1 the same as 10.1.1.1?
No. In the legacy URL format a leading zero makes that part octal, so 010 is 8 and the address is 8.1.1.1. RFC 3986 does not allow leading zeros in the dec-octet grammar at all.
How do I normalize an IP address safely?
Parse it into a number or an address object, then compare the numeric value. Strict parsers such as Python ipaddress.ip_address or Node net.isIP reject the legacy spellings, while inet_aton and URL parsers accept them.
Is 2852039166 a valid IP address?
It is the decimal form of 169.254.169.254, the link-local address commonly used for cloud instance metadata. Any filter that blocks the dotted string but not the integer leaves that endpoint reachable.
Why do browsers still accept these formats?
The WHATWG URL standard specifies the step-by-step algorithm that accepts one to four parts in decimal, octal or hexadecimal. It exists for web compatibility with how browsers historically parsed hosts.
Related articles
- Which HTTP status code should an API return?
Address handling sits next to redirects and header parsing, where small normalisation gaps become security bugs.
- Regex groups and lookarounds explained
Most IP filters are regular expressions. Here is why matching the text is not the same as matching the value.
Related tools
- Number Base Converter
Decimal, hexadecimal, octal and binary side by side.
- Regex Tester
Check what your IP filter pattern really matches.
- Diff Checker
Compare a raw host string with its normalised form.
- URL Decoder
Decode percent-encoding before you compare anything.