deniz.in

Markets

Weather

Loading weather

· via Hacker News – Front Page (native)

CPython fixed an IDNA flaw where str.lower() used the wrong Unicode version

Seth Larson explains how CPython's IDNA codec called str.lower(), which follows the interpreter's modern Unicode data instead of the Unicode 3.2 rules the spec requires, producing non-compliant domain encodings.

CPython fixed an IDNA flaw where str.lower() used the wrong Unicode version

A standards bug hiding in plain sight

Seth Larson, a CPython maintainer working as Security Developer-in-Residence at the Python Software Foundation, has written up a case where one of Python's most ordinary string operations became a security flaw. According to Larson, whose post reached the Hacker News front page, the problem sits in the standard library's handling of internationalized domain names, and it comes down to which version of Unicode str.lower() consults when it lowercases a character.

Background: mapping Unicode onto ASCII domains

Many core internet standards accept only ASCII, yet domain names are written in scripts far beyond the Latin alphabet. That gap is bridged by IDNA, Internationalizing Domain Names in Applications. Larson explains that its older form, IDNA 2003, relies on NamePrep — defined in RFC 3491 as a profile of the StringPrep algorithm from RFC 3454 — to normalize and case-fold text before converting it to an ASCII-compatible form. IDNA 2003 has since been superseded by IDNA 2008, defined across RFCs 5890 through 5893.

Python exposes both worlds. The built-in idna codec, used via str.encode('idna'), implements IDNA 2003, while the third-party idna package on the Python Package Index implements IDNA 2008. Larson's general guidance is to reach for the package rather than the codec, unless an application deliberately needs the older behavior.

Where the mismatch creeps in

StringPrep's case-folding step, described in section 3.2 of RFC 3454, maps characters through tables B.2 and B.3. Table B.2 amounts to lowercasing every character according to Unicode rules, while B.3 holds the exceptions. Python's stringprep module in the standard library applies the B.3 exceptions and then calls str.lower() on whatever remains.

That fallback is the bug. str.lower() uses the Unicode database shipped with the running interpreter — Larson's reports unicodedata.unidata_version as 17.0.0 — but the RFC's tables are effectively Unicode 3.2.0 case-folding rules frozen in place. The specification is pinned to a snapshot, while one step of the implementation drifts forward with each Python release. Notably, Python already ships a frozen Unicode 3.2.0 database, unicodedata.ucd_3_2_0, which both stringprep and the idna codec import for their lookups; the trailing .lower() call simply bypasses that pinning.

The divergence is observable. For the character (U+13A0), Larson shows that "ᎠᎠ".encode("idna") produces xn--58da when the RFC 3454 rules are followed, but xn--kz9aa if Unicode 17.0.0 case folding is applied. The same input string can therefore encode to different ASCII domains depending on the interpreter's Unicode data — a gap between what the standard demands and what the code does, which Larson classifies as a vulnerability.

The remediation

The fix Larson describes makes the StringPrep path behave as though it were pinned to Unicode 3.2.0. Working through the codepoints, he and Stan Ulbrych recorded every position where the interpreter's modern str.lower() differs from the 3.2.0 behavior and encoded new exceptions so that this particular function produces the older results. The issue was reported by Bitshift, the remediation was reviewed by Marc-Andre Lemburg and Petr Viktorin, and it is tracked as CVE-2026-17084. Larson adds that his security work at the Python Software Foundation is sponsored by Alpha-Omega.

Why it matters

The episode is a reminder that "just lowercase it" is not a neutral operation. str.lower() is coupled to the Unicode data bundled with each interpreter, which changes across releases, so any algorithm built on a frozen Unicode snapshot must use snapshot-pinned tables at every step rather than only some of them.

For developers, the practical takeaways are straightforward. New code should prefer the third-party idna package, which implements the current IDNA 2008 rules, over .encode("idna"). And any system that has relied on the legacy codec should be aware that encoding of certain names could differ between Python builds, meaning two implementations could silently disagree on whether the same domain string matches — precisely the kind of inconsistency that matters in name validation and comparison code.

  • #python
  • #security
  • #unicode
  • #idna
  • #cve

Related posts