How str.lower() Became a Python Vulnerability
Translated by a model; the original is in Russian.
Seth Larson, who handles security at the Python Software Foundation, has dissected an instructive case: an innocent-looking str.lower() call in the standard library turned out to be a vulnerability, assigned the identifier CVE-2026-17084.
Where the roots run
Many internet standards have historically understood only ASCII, whereas the world writes in far more than the Latin alphabet. To map domain names from different scripts onto ASCII, the IDNA mechanism was devised. Its older version, IDNA 2003, relies on the StringPrep algorithm (RFC 3454) and the NamePrep profile (RFC 3491). Python supports it through the idna codec (str.encode('idna')), and the newer IDNA 2008 through a separate idna package from PyPI.
One of the steps in StringPrep is “case folding” — that is, bringing characters to a single case so they can be compared without regard to upper- and lowercase letters. For characters that didn't fall into the table of exceptions, the implementation in the stringprep module simply called code.lower().
Where the catch is
The problem is that str.lower() uses the version of the Unicode database that the particular interpreter was built with. You can find it out through unicodedata.unidata_version — in recent builds this is already 17.0.0. Meanwhile StringPrep is rigidly tied to the rules of Unicode 3.2.0: tables B.2 and B.3 from RFC 3454 are, in essence, the case rules of exactly that version, fixed by hand.
The discrepancy between the implementation and the specification is precisely the vulnerability. Larson gives a vivid example: the two-character string «ᎠᎠ» (U+13A0, from the Cherokee alphabet) should, by the standard, be encoded as xn--58da, but using the rules of Unicode 17.0.0 it yields a completely different result — xn--kz9aa. For a mechanism that decides which domain a typed-in name corresponds to, such a discrepancy is a direct path to confusion and potential attacks.
How it was fixed
The cure turned out to be straightforward: the developers walked through every Unicode code point and recorded in a separate exceptions table those cases where the behavior of str.lower() in the current version of Python differs from Unicode 3.2.0. Now the function behaves as though it were using the old database, and IDNA 2003 once again conforms to the standard.
Larson thanks Bitshift for reporting the vulnerability, Stan Ulbrych for co-developing the fix, and Marc-André Lemburg and Petr Viktorin for the review. His work at the PSF is funded by the Alpha-Omega initiative.
The moral of the story is simple: where the standard requires a fixed version of Unicode, “ordinary” lowercasing stops being ordinary.
Source: Lobsters
Comments
Sign in, to comment.