Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

Python validate=True accepted two Base64 spellings? Enforce canonical padding bits

Python’s strict alphabet check and RFC 4648 canonical encoding are different guarantees. Use canonical=True on Python 3.15+, or decode and re-encode exactly on older releases.

Original diagram showing syntax validation, byte decoding and canonical Base64 validation as separate gates.
Original illustration: validate the alphabet, decode the bytes, then enforce one canonical spelling. Watch at Ease

validate=True is a strict alphabet-and-padding syntax check, but on Python 3.14 and earlier it can still accept Base64 aliases whose unused pad bits are non-zero. Those strings decode to the same bytes even though only one can be emitted by a conforming encoder.

Use canonical=True with Python 3.15, or decode then re-encode and compare exactly on older releases. Choose the alphabet and padding policy first, and never confuse canonical spelling with authenticity.

The surprising result is real

On CPython 3.12.14, base64.b64decode accepts wQ==, wR== and wc== with validate=False or validate=True; all three decode to the single byte c1. validate=True rejects characters outside the selected alphabet and malformed padding placement, but in Python through 3.14 it does not prove that unused pad bits are zero.

This is not a byte collision in the encoder. A conforming encoder emits one canonical spelling. The many-to-one behavior comes from a permissive decoder ignoring the low four bits of the second symbol when two equals signs mean that only one output byte is significant.

Why pad bits create aliases

Base64 groups input into 24 bits and emits four 6-bit symbols. For a final group containing one byte, only eight bits carry data: six in the first symbol and the top two in the second. The remaining four bits in that second symbol are pad bits. RFC 4648 requires conforming encoders to set them to zero, which makes the text canonical.

The RFC also lets decoders choose whether to reject non-zero pad bits. That distinction matters: syntax may be acceptable and decoded bytes may be correct while the textual representation is still non-canonical.

Python 3.15: request the canonical check

Python 3.15 adds canonical=True to base64.b64decode and binascii.a2b_base64. Use base64.b64decode(value, validate=True, canonical=True) when your protocol requires the standard alphabet, normal padding and zero pad bits. A non-canonical alias then raises binascii.Error instead of silently collapsing to the same bytes.

Keep validate=True as well. canonical=True addresses the unused-bit problem; validate=True addresses alphabet and structural errors. State whether your protocol permits missing padding, ignores whitespace or uses the URL-safe alphabet instead of assuming one universal Base64 policy.

Python 3.14 and earlier: decode, re-encode, compare

For a padded standard-Base64 field, first decode with validate=True, then re-encode the decoded bytes with base64.b64encode and require exact byte-for-byte equality with the input. That rejects wR== and wc== because the encoder produces wQ==. Convert text to ASCII explicitly and reject non-ASCII input before decoding.

Do not “fix” arbitrary input by appending several equals signs before the comparison. If your protocol intentionally uses URL-safe or unpadded Base64, define that policy first, normalize once, decode with the matching alphabet, and compare with the matching canonical encoder. A fallback is only correct relative to a specific protocol.

Where canonical spelling matters—and where it does not

Canonical text matters when the Base64 string itself is a database key, cache key, deduplication value, allow-list entry, signature input or audit identifier. If one layer compares text while another compares decoded bytes, aliases can split identity decisions. Store or compare decoded bytes when possible; otherwise enforce one canonical string at the boundary.

Canonicalization does not authenticate data. An attacker can create a perfectly canonical string containing malicious bytes. Verify a MAC or digital signature over the exact protocol-defined bytes, and keep authorization rules separate from Base64 parsing.

A compact acceptance test

Test the canonical value, at least one non-zero-pad-bit alias, invalid alphabet characters, misplaced or excess padding, empty input if allowed, and every protocol variant you support. On Python 3.15+, assert canonical=True rejects the alias. On older Python, assert decode-and-re-encode rejects it.

Log the rejection category without logging secrets. A successful migration means every supported runtime accepts the same canonical examples, rejects the same aliases, and produces the same decoded bytes for accepted input.

Original comparison matrix for validate, canonical checks, re-encoding and cryptographic authentication.
Original illustration: canonicalization gives one spelling; a signature or MAC is still required for trust. Watch at Ease