Safeguard
Application Security•

CWE-1389 Incorrect Parsing of Numbers with Different Radices

The IP address 0127.0.0.1 is not 127.0.0.1. How leading zeros and 0x prefixes bypass access checks, and how to parse numbers strictly.

Safeguard Security Team
5 min read

CWE-1389 covers code that parses numeric input assuming base 10 and does not account for inputs written in another base, called a radix.

If you accept IP addresses, netmasks, ports, IDs or any number from outside your system, this can reach you. It has been behind real access control bypasses and server side request forgery in popular libraries.

How it happens

Many parsers treat a number that starts with 0 as octal, and one that starts with 0x as hexadecimal. The classic example is the C function inet_addr().

The arithmetic is simple. The string "023" read as octal is 19 in decimal, and "0x31" read as hexadecimal is 49. The bigger problem is IP addresses. "0127.0.0.1" is parsed as octal because of the leading zero, which gives the same value as 87.0.0.1, not 127.0.0.1.

So two parts of a system can disagree. Your validator reads one thing, the code that actually connects reads another. A rule such as "block 127.0.0.1" can be bypassed, and an allowlist can pass an address you never meant to allow.

The observed examples in the record are all variations of this:

  • CVE-2021-29662, a chain where zero prepended IP addresses in a Perl IP validation module lead to an access control bypass.
  • CVE-2021-28918, zero prepended addresses in a product that manages IP blocks leading to SSRF.
  • CVE-2021-29921, the same idea in a Python standard library package, leading to SSRF.
  • CVE-2021-29923, the net library in Go, leading to an access control bypass.

Perl, Python and Go, one mistake.

What an attacker gains

From the record's consequences:

  • Confidentiality. An unexpected numeric base can be used to reach private application resources.
  • Integrity. It can bypass or manipulate access control mechanisms.

MITRE adds that the exact impact depends on the surrounding code. Network access control through unexpected IP addresses or netmasks is the headline case, and apparently symbolic identifiers being processed as numbers is another.

How to find it

MITRE says static analysis (SAST) can find some instances by building a model of data flow and control flow from source or compiled code. In practice you can also check by hand.

  1. Search for every place you parse a number from text, especially IP addresses, netmasks and IDs.
  2. Note which function does the parsing, and which base it assumes.
  3. Feed it "0127.0.0.1", "0x7f.0.0.1" and "017" and compare the result with what your validator believed.
  4. Check whether the validator and the code that uses the value use the same parser.

How to fix it

MITRE lists three approaches.

Convert first. If only decimal values are expected, convert every numeric string to an explicit base 10 integer before any conditional check. Then octal and hex strings never reach the comparison.

Validate when you support more than one base. If you do need other bases, look for the leading markers you want to support (such as 0x for hex), convert with the matching base, and reject every other alternative base string.

Anchor your regular expressions. If a regex validates IP addresses, bound it with ^ and $ so base prepended addresses cannot match as a substring.

Here is the vulnerable shape and a stricter version.

/* Vulnerable pattern: inet_addr() accepts octal and hex forms.
   "0127.0.0.1" is read as 87.0.0.1, not 127.0.0.1. */
in_addr_t a = inet_addr(user_supplied);
import re

# Fixed: accept only canonical decimal dotted quads, no leading zeros.
OCTET = r"(0|[1-9][0-9]{0,2})"
IPV4 = re.compile(rf"{OCTET}(\.{OCTET}){{3}}")

def parse_ipv4(text):
    if not IPV4.fullmatch(text):
        raise ValueError("not a canonical decimal IPv4 address")
    parts = [int(p, 10) for p in text.split(".")]
    if any(p > 255 for p in parts):
        raise ValueError("octet out of range")
    return parts

The point is to validate and to parse in one place, in one base, and to use that parsed value for the decision. Do not validate a string and then pass the original string on to a different parser.

Try it yourself

Pick a function in your own code that checks an IP address against a blocklist. Give it the leading zero form of an address on that list and see whether the check still fires. The SAST demo below walks through a source scan, which is one way to surface risky parsing code.

Click through it at your own pace, or open it full screen.

Quick check

Why does 0127.0.0.1 bypass a check for 127.0.0.1?

The leading zero makes some parsers read the first part as octal, which equals 87 in decimal. The validator and the connecting code can disagree about what address it is.

Does anchoring a regex with ^ and $ fully solve this?

It stops base prepended addresses from matching as a substring, but MITRE also recommends converting to an explicit base 10 integer before comparing.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.