Advertisement
Four CVEs came out of this one detail

IP to Octal Converter
and Why 0177.0.0.1 Means Three Things

Convert an IPv4 address to octal and back, one at a time or a whole column. Then use the third tab for the part that actually matters: the same string with a leading zero resolves to 127.0.0.1 for your operating system, 177.0.0.1 for several standard libraries that shipped for years, and nothing at all for the versions that got patched. That disagreement earned four CVEs in 2021.

Try one:

Try one:

Paste a column, either direction. Dotted decimal comes back as octal, octal comes back as dotted decimal, and the direction is decided line by line.

Up to 1,000 lines.
InputConvertedDirection

These are the documented behaviours from the 2021 CVE wave, applied to whatever you type. Nothing is executed and nothing leaves your browser.

Try one:

Quick Answer: How Do You Convert an IP to Octal?

Convert each octet to base-8 and put a zero in front of it. 127.0.0.1 becomes 0177.0.0.1. The leading zero is what tells a parser to read the number as octal — and whether it actually does is the interesting part.

Advertisement
Robert Harrison, OSINT and Network Utility Expert, on octal IP addresses and leading zero parsing at TrustMyIP.com
Written & Verified By

Robert Harrison

OSINT and Network Utility Expert

Robert works on network diagnostics and the practical end of IP tooling — the conversions, lookups and calculations that sit underneath everything else.

Octal is the least used of the IP notations and by far the most interesting, because it is the one where software disagrees with itself. Nobody writes 0177.0.0.1 by choice. It turns up when somebody is testing whether your validator and your HTTP client read a string the same way — and for a long stretch, in most languages, they did not.

What makes this worth a page rather than a footnote is that it was never one bad library. Python, Go, Rust and the npm ecosystem all shipped the same assumption and all got a CVE for it in the same year. The converter above is ordinary; the third tab, which shows you what each of them would have done with the string you typed, is the part I would actually keep.

Last reviewed 24 August 2026 · Runs entirely in your browser · Nothing you type is sent anywhere

View all articles by Robert Harrison
Advertisement

Why Does One String Mean Three Different Things?

Because there is no single rule for what a leading zero in an IP address means, and three families of software settled on three different answers.

Your operating system inherited inet_aton, which reads a leading zero as base-8. To it, 0177.0.0.1 is 127.0.0.1, and ping 0177.0.0.1 reaches localhost. Browsers follow the same rule, so http://0177.0.0.1/ does too.

Several standard libraries did something else entirely. They stripped the zero and read the rest as decimal, which turns 0177.0.0.1 into 177.0.0.1 — a publicly routable address that has nothing to do with localhost. Not a rounding error. A different machine.

And the patched versions of those same libraries now refuse the string altogether. Ambiguous input is safer refused than guessed at, because every guess disagrees with somebody.

Here is the shape of the bug. A validator written in one of those languages reads 0177.0.0.1, sees 177.0.0.1, decides it is a public address and allows it. The HTTP client then takes the original string and asks the operating system to resolve it — which gives 127.0.0.1. The request goes to localhost. Nothing was hacked. Two components simply answered different questions about the same eleven characters.

The leading zero is not decoration

This is worth stating because several octal converters get it wrong. Convert 192.168.1.1 on some of them and you are given 300.250.1.1, or 300.250.001.001. Both look like octal. Neither works.

Paste either into a browser, a ping command or any resolver and it is refused, because without the leading zero on the first group, 300 is read as decimal — and 300 is not a valid octet. The zero is the marker that switches the parser into base-8. Remove it and the output is a number that resembles octal and resolves to nothing.

The converter above outputs 0300.0250.01.01, which does work, and a zero-padded variant alongside it. Both round-trip. You can check either by pasting it back into the second tab, or into ping.

For more than one address, the bulk tab takes up to a thousand lines in either direction and reads a .txt file if you drop one on it. Direction is decided per line, so a mixed column of dotted decimal and octal converts in a single pass — useful when auditing a firewall config or a list of allowed hosts for exactly the ambiguity this page is about. To put that list in numeric order first, the IP sorter handles it.

Why anyone would care about localhost

Because the interesting targets are never public. A server-side request forgery filter — SSRF for short — exists to stop an application being talked into fetching internal resources.

That means an admin panel on the loopback address, a database on a private range, or the cloud metadata endpoint on 169.254.169.254, which on an unprotected instance hands out credentials to anyone who asks.

SSRF is where this matters most because the filter is the only thing standing between a user-supplied URL and the inside of your network. Every other use of octal is cosmetic. This one decides whether a request that should have been refused gets made.

In octal that metadata address is 0251.0376.0251.0376. A filter matching the string 169.254.169.254 does not stop it, and neither does one that blocks the 169.254.0.0/16 range if it never resolves the input to an address in the first place.

Octal is not the only notation this works with. The same address can be written in base-16 or as a plain integer, and both turn up in phishing links for a simpler reason: URL obfuscation, where the point is only that a person cannot read the destination. The hex converter covers that side, including the @ sign that hides a host in plain sight, and percent-encoding hides the rest of the URL the same way.

What makes octal different is that it does not merely disguise an address from a reader. It makes two pieces of software genuinely disagree about which address it is — and only one of them is the one that fetches.

Which CVEs Came Out of One Leading Zero?

In 2021 this stopped being a curiosity. Researchers Victor Viale, Sick Codes, Kelly Kaoudis, John Jackson and Nick Sahler worked through the standard libraries and found the same assumption in all of them. The Python entry carries a CVSS base score of 9.8, which is as close to the top of the scale as a library bug gets.

CVE Where What it did with 0177.0.0.1 Fixed in
CVE-2021-29921CVSS 9.8Python ipaddressStripped the zero → 177.0.0.13.8.12, 3.9.5, 3.10.0a7
CVE-2021-29923Go net.ParseIPRead it as decimal → 177.0.0.1Go 1.17
CVE-2021-29922Rust std netSame classAfter 1.52
CVE-2021-28918Criticalnpm netmaskSame class, in a package used by 270,000+ projectsPatched 2021

The Perl equivalent, Net::Netmask, got its own entry as CVE-2021-29424. The netmask disclosure came first and is what finally moved the others — the Python issue had been sitting open on the bug tracker since March 2019.

Go's release note says it better than any advisory

When Go 1.17 started rejecting these addresses, the release note explained why in a paragraph worth reading twice:

The components "were always interpreted as decimal, but some operating systems treat them as octal. This mismatch could hypothetically lead to security issues if a Go application was used to validate IP addresses which were then used in their original form with non-Go applications which interpreted components as octal."

That is the entire vulnerability class in three sentences. It is not about octal. It is about one program validating a string and another program resolving it, with nobody checking that they agree.

What it broke when it was fixed

Kubernetes ran into this directly. Once Go 1.17 began rejecting leading zeros, clusters holding a value like 010.0.0.1/32 in a network policy discovered it had been silently read as 10.0.0.1/32 the whole time. Kubernetes had to fork the older parser to stay compatible with data already stored, then work out a path to deprecating those values.

Which is a useful reminder that the ambiguity was never theoretical. It was sitting in production configuration, meaning something different from what whoever typed it intended.

How Do You Make Your Own Code Safe?

The instinct is to handle octal correctly. That is the wrong fix, and Go's maintainers said so plainly: re-encode values after validation. It is one sentence and it removes the entire class.

The rule

Parse the input into an address object. Then serialise that object back into a string, and pass the serialised form to everything downstream. Never let the user's original text past the validator.

The moment two components each parse the raw input, you have this bug again — whether the notation is octal, hex, a bare integer or something nobody has thought of yet. Re-encoding means only one parser ever sees user text, and everything after it works from a canonical form.

# WRONG - the validator and the request each parse the raw string import ipaddress, requests addr = ipaddress.ip_address(user_input) if not addr.is_private: requests.get(f'http://{user_input}/') # <- original text, parsed again # RIGHT - downstream only ever sees the canonical form addr = ipaddress.ip_address(user_input) # raises on leading zeros in 3.9.5+ if not addr.is_private: requests.get(f'http://{addr}/') # <- re-encoded, unambiguous

The difference is one character, and it is the whole fix. {user_input} carries whatever was typed; {addr} carries what the validator actually approved.

And check what you are running on

# Python 3.9.5 and later - the safe behaviour >>> ipaddress.ip_address('0177.0.0.1') ValueError: Leading zeros are not permitted in '0177' # Python 3.9.4 and earlier - the vulnerable behaviour >>> ipaddress.ip_address('0177.0.0.1') IPv4Address('177.0.0.1') # wrong host, no warning

If that second output is what you get, the interpreter is old enough to matter. The same check applies to Go before 1.17 and Rust at or below 1.52.

A blocklist is the wrong shape for this anyway. Comparing text against a list of forbidden addresses fails the moment somebody writes one differently, and there are more ways to write 127.0.0.1 than anybody wants to enumerate. Resolve to an address object first, then test that object's properties — is_private, is_loopback, is_link_local — rather than matching strings. The decimal converter covers the same principle from the database side, where storing the canonical integer removes the ambiguity permanently.

What this page cannot tell you

The parser comparison is a model of documented behaviour, not a live test of your stack.

Nothing is executed

The comparison applies the behaviours documented in each CVE to whatever you type. It does not run Python, Go or Rust, and it cannot know which versions your own systems have. Check those yourself with the snippet above.

Libraries beyond the standard ones vary

Four CVEs covered the standard libraries. Every HTTP client, proxy, WAF and framework has its own parsing, and some of them still disagree. The only safe assumption is that they disagree until you have tested them.

Rejection is not protection

A patched library refusing a leading zero closes one door. Hex, plain integers and short forms all still parse everywhere, so a filter that only learned about octal has learned very little.

Octal is legitimate too

It predates the web and appears in older scripts and documentation without any bad intent. Seeing it in an access log is worth a second look; seeing it in a config file written in 1998 is not a finding.

Once you have resolved an address, a geolocation and network lookup tells you whose it is, and a blacklist check tells you whether it has been reported.

To see the same address as bits, the binary converter lays out all thirty-two.

And the CIDR calculator turns a prefix into the range it covers. That is the shape an allowlist should take — a range, not a list of strings that each have to be spelled one particular way.

Frequently asked questions about octal IP addresses

How do I convert an IP address to octal?

Convert each octet from decimal to base-8 and prefix it with a zero, which is the marker that says "read this as octal". 127.0.0.1 becomes 0177.0.0.1, because 127 in base-8 is 177. The converter above does all four octets as you type, and shows the working.

Is 0177.0.0.1 the same as 127.0.0.1?

To your operating system, your browser, curl and ping — yes. To a Python program running anything before 3.9.5 — no, it was 177.0.0.1, a completely different and publicly routable address. To Go 1.17 or later — neither, it is rejected outright. One string, three answers. That disagreement is the whole reason this page exists.

Why is a leading zero in an IP address a security problem?

Because a validator and the thing that later makes the request can disagree about what the string means. A filter reads 0177.0.0.1 as 177.0.0.1, sees a public address and allows it. The HTTP library then resolves the original string as 127.0.0.1 and fetches from localhost. The filter was never bypassed by a clever payload — it simply answered a different question. That is SSRF by parser disagreement, and it earned four CVEs in 2021.

What is CVE-2021-29921?

The Python one, rated 9.8 Critical. Python’s ipaddress library stripped leading zeros instead of interpreting them, so ip_address('0177.0.0.1') returned 177.0.0.1. Any access control built on that library could be walked past. It affected 3.8.0 to 3.8.11, 3.9.0 to 3.9.4 and the early 3.10 alphas, and was fixed in 3.8.12, 3.9.5 and 3.10.0a7.

Does Python still accept leading zeros?

No, and the fix is worth understanding. Python did not start reading them as octal — it started rejecting them. ipaddress.ip_address('0177.0.0.1') now raises ValueError. Go 1.17 made the same call for net.ParseIP. The reasoning is that an ambiguous input is safer refused than guessed at, since any guess will disagree with something.

Which languages were affected?

Four CVEs landed in the same 2021 wave: CVE-2021-29921 for Python, CVE-2021-29923 for Go, CVE-2021-29922 for Rust, and CVE-2021-28918 for the npm netmask package, which was used by over 270,000 projects. The Perl equivalent got CVE-2021-29424. It was not one bad library — it was an assumption almost everybody had made.

How do I make my own code safe?

Re-encode after validating. Go’s own release note puts it best: always re-encode values after validation, which avoids this class of parser misalignment entirely. Parse the input into an address object, then serialise that object back to a string and pass the serialised form onward. Never carry the user’s original text past the validator — the moment two components each parse the raw string, you have this bug again.

Do browsers still accept octal IP addresses?

Yes. http://0177.0.0.1/ reaches localhost in mainstream browsers, and ping 0177.0.0.1 works from a normal shell. The behaviour comes from inet_aton and predates the web. It is not deprecated and there is no sign of it being removed.

What does the octal form of common addresses look like?

127.0.0.1 is 0177.0.0.1. 192.168.1.1 is 0300.0250.01.01. 10.0.0.1 is 012.0.0.1. 169.254.169.254 — the cloud metadata address that SSRF filters exist to protect — is 0251.0376.0251.0376. Zero octets stay as a single 0, since 00 and 0 mean the same thing.

Does octal work for IPv6?

No, and any tool offering it is producing something you cannot use. IPv6 is defined in RFC 4291 as groups of four hexadecimal digits, and no specification defines an octal form — no resolver, browser or library will accept one. Running the number through a base-8 conversion produces digits, but they cannot be pasted anywhere. This page handles IPv4, which is where octal notation genuinely resolves.

Why do some converters output 300.250.1.1 instead of 0300.0250.01.01?

Because they convert the digits and forget what the leading zero is for. 300.250.1.1 looks like octal and resolves nowhere — without the zero, a parser reads 300 as decimal, and 300 is not a valid octet. The zero is the marker that switches the parser into base-8. Anything you can actually paste into a browser or a ping command has to carry it.

Can I convert a whole column of addresses to octal?

Yes. The list tab takes up to a thousand lines, in either direction — dotted decimal in, octal out, or octal in, dotted decimal out. Direction is decided per line, so a mixed column works. Results copy as tab-separated text or download as CSV.

Related tools

The other notations, and the rest of the address toolkit.

Browse the full set on the TrustMyIP tools directory.

Last updated 24 August 2026 · Runs in your browser · Nothing you type is sent anywhere