May 3, 2021·4 min read·engineeringpythonsecuritynetworking

Python 3.9.5 rejects ambiguous IPv4 octets

Python 3.9.5 stops accepting leading zeros in IPv4 strings. The change prevents a validator and a network library from assigning different addresses to the same text.

An address validator can approve one destination while a network library connects to another if they interpret the same characters differently. Leading zeros in IPv4 octets create that possibility because some parsers treat them as octal notation. Python 3.9.5, released on May 3, tightens the ipaddress module to reject that ambiguity. A string that previously produced an address may now fail validation.

The release changelog places the change under security and identifies issue 36384. It explains that socket.inet_aton treats leading zeros as octal, while glibc's inet_pton rejects leading zeros. The maintenance release therefore changes accepted input to bring the library's parser into agreement with a stricter interpretation.

I am comfortable with a compatibility change that rejects an address whose meaning depends on the next library. Python is often useful because several libraries can cooperate inside one application. That cooperation becomes a security problem when validation and execution assign different meanings to the same input.

The ambiguity exists before the connection

An IPv4 address string commonly contains four decimal components separated by dots. A component such as 16 names a decimal value. Under octal interpretation, 016 names a different value, even though a decimal parser can also turn it into an integer. A leading zero therefore does more than add formatting when the string crosses parser boundaries.

Suppose an application uses a parsed address to decide whether a destination is acceptable and later passes the original string to another library. If the second library interprets a component differently, the earlier decision does not describe the address actually used. This is a consequence of the parser disagreement documented in the changelog. Whether an application is exploitable depends on how its validation result reaches its connection logic.

The change rejects the input before that disagreement can persist through ipaddress. It does not change the meaning of an ordinary decimal address. A component consisting of a single zero remains valid, because zero itself has no redundant prefix to interpret.

from ipaddress import IPv4Address, AddressValueError

assert str(IPv4Address("192.168.0.1")) == "192.168.0.1"

try:
    IPv4Address("192.168.000.001")
except AddressValueError:
    print("Ambiguous IPv4 text rejected")

This example describes Python 3.9.5's behavior. It intentionally uses IPv4Address so the expected address family and exception are explicit. The rejected string contains familiar decimal-looking values, but the parser now refuses their representation rather than trying to determine the caller's intention.

The check applies to each octet

The versioned implementation puts the rule in _parse_octet. It first rejects an empty component and requires ASCII decimal digits. It then checks the component length, rejects a leading zero unless the component is exactly 0, and converts the remaining text using base ten. The converted value must also fit the permitted octet range.

That ordering gives the rule a narrow meaning. A leading zero is an error even if decimal and octal interpretations would happen to produce the same number. The implementation does not attempt to preserve ambiguous syntax on a case-by-case basis. It establishes a grammar in which every accepted multi-digit component starts with a nonzero digit.

The release's tests cover leading zeros at different positions. They include forms with padding in the first component and forms padded only in later components. This matters because inspecting only the start of the full address would miss ambiguity inside the remaining octets. The implementation checks the components independently.

The same test file distinguishes IPv4 from IPv6 behavior. IPv6 examples with leading zeros remain valid and can have a shorter printed representation. The security change concerns the decimal-versus-octal ambiguity in IPv4 strings. It is not a general ban on zeros at the beginning of all textual network-address components.

Normalization has to reach the consumer

The implementation can construct an address from an integer or packed bytes as well as from text. Those forms already identify an address without asking a decimal-string parser to infer the meaning of a prefix. This distinction helps explain why textual acceptance is a separate concern from the address object's stored value.

A system that validates text and then discards the parsed result has two interpretations to maintain. Passing a normalized representation derived from the accepted address can reduce that disagreement, provided the consumer uses the same address family and representation. This is an architectural consequence of the fix, not a guarantee that normalization alone solves every destination-policy problem.

Existing configuration files with zero-padded IPv4 addresses may need correction. Rejecting those strings is the intended behavior, so restoring acceptance through an alternate permissive parser would reintroduce the ambiguity. The unpadded form states the decimal components directly and gives downstream code less syntax to interpret.

I prefer address handling that preserves a parsed value through the decision that authorizes its use. Repeatedly returning to the original string creates another opportunity for a different parser to disagree. Python 3.9.5 removes one known disagreement at input acceptance, with an explicit exception for text that no longer satisfies the library's IPv4 grammar.