Luke Oliff.

When Python Case Folding Becomes a Security Bug

·Security·3 min read·Luke Oliff
TL;DR

str.lower() and str.casefold() use whatever Unicode version your interpreter shipped, not the version a spec expects. That mismatch let one hostname fold two ways, which is how case folding turns a trust check into a bypass. CVE-2026-17084 is a real one.

Python case folding looks harmless. You call str.lower() or str.casefold() to compare two strings without caring about capitalisation, and most days that is fine. But Unicode case folding is not ASCII case folding, and in August 2026 Seth Larson wrote up a real one: CVE-2026-17084, where Python’s IDNA handling folded with the interpreter’s current Unicode version instead of the Unicode 3.2 rules the spec demands, so one hostname could fold two different ways.

That gap is small. The consequences are not.

The footgun in three lines

Some non-ASCII characters lower to ASCII letters. The Kelvin sign is the classic example: it renders like a capital K, but it is codepoint U+212A, and Python lowers it to a plain k.

>>> "K"            # U+212A, the Kelvin sign
'K'
>>> "K".lower()
'k'
>>> "K".lower() == "k"
True

casefold() goes further, because it exists to fold aggressively for caseless matching. The German eszett folds to two letters:

>>> "ß".casefold()
'ss'

So two strings that share no bytes can compare equal after folding, and one string can fold to something a different system never sees. That is the whole bug class.

Where Python case folding turns into a bypass

Picture an allowlist check written the obvious way:

ALLOWED_HOST = "keys.internal"

def is_allowed(host: str) -> bool:
    return host.lower() == ALLOWED_HOST   # looks safe

Send "Keys.internal", a Kelvin sign followed by eys.internal. It lowers to keys.internal, so is_allowed returns True. Then the code that acts on the value, a DNS resolver, an HTTP client, a downstream service, sees the original string with the raw Kelvin sign in it. Your security layer said yes based on a folded form the network layer never handles. That disagreement between two parsers is exactly what Larson’s IDNA case is: the trust decision and the resolution decision fold the string differently.

The same shape shows up in username uniqueness, email matching, and anything where you casefold to dedupe. PEP 672 documents these Unicode traps in Python specifically, and it is worth reading once so the category stops surprising you.

The fix

The blunt rule: do not use lower() or casefold() as a security boundary on untrusted Unicode. They are for display and for loose human-facing matching, not for deciding trust.

For hostnames, encode through IDNA with a library that enforces the correct profile, or restrict input to ASCII and reject anything else outright. Rejecting is underrated. If the value should be an ASCII hostname, a non-ASCII character is an error, not something to normalise into shape.

When you genuinely must compare Unicode, normalise with the exact algorithm the consumer will use, unicodedata.normalize with an explicit form plus a codepoint allowlist, and compare that. Never fold one way for the check and let something else fold another way for the action.

Python’s own remediation, per Larson’s writeup, was to give stringprep explicit exceptions so its folding behaves as Unicode 3.2 regardless of the interpreter’s Unicode version. That is the spec-correct answer for IDNA. For your own code, the lesson is smaller and sharper: a .lower() in front of an equality check is not a safe way to compare anything an attacker controls.