当 str.lower() 在 Python 中成为安全漏洞时 – Seth Larson
When str.lower() is a security vulnerability in Python – Seth Larson

原始链接: https://sethmlarson.dev/when-str-lower-is-a-security-vulnerability

国际化域名 (IDNA) 需要将 Unicode 映射为 ASCII。Python 传统的 `idna` 编解码器(IDNA 2003)依赖于 StringPrep 算法,该算法严格要求使用 Unicode 3.2.0 的大小写转换规则,以确保跨平台行为的一致性。 由于 Python 内置的 `str.lower()` 方法使用的是解释器当前的 Unicode 版本,而非规范要求的版本,因此发现了一个漏洞(CVE-2026-17084)。这种差异会导致不同的 Unicode 版本产生冲突的域名映射,从而可能引发安全绕过问题。 此次修复工作包括识别当前 Unicode 规则与传统的 Unicode 3.2.0 标准之间的所有差异。开发人员创建了一个异常映射表,专门用于在执行 StringPrep 任务时强制 `str.lower()` 模拟 3.2.0 的行为。尽管现代的 `idna` 软件包(IDNA 2008)是推荐标准,但此修复确保了传统的 IDNA 2003 实现与其规范保持一致。建议用户优先使用 `idna` 软件包,而非已弃用的 `str.encode("idna")` 方法。

Hacker News 上的一场讨论探讨了 Seth Larson 最近发布的一篇文章,内容关于 Python 的 `str.lower()` 方法如何可能引发安全漏洞。该问题源于“解析器差异”(parser differentials),即系统的不同组件或不同的编程语言在处理 Unicode 字符或域名(IDNA 2003)时,解释方式存在不一致。 如果安全过滤器(如 WAF 或身份验证模块)因这些 Unicode 版本的不一致,而对字符串的处理方式与后端应用程序不同,攻击者就可能绕过过滤器、伪造域名或引发服务端请求伪造(SSRF)。 虽然一些评论者认为这只是一个“正确性错误”,但专家强调,在复杂的异构系统中,不一致的数据解释是众所周知的漏洞先兆。当技术栈的两个部分对输入的“归一化”版本理解不一致时,它们之间的安全边界实际上就失效了。舆论共识是,尽管这种风险因情况而异,但开发人员在处理域名或涉及安全的关键标识符等敏感输入时,必须谨慎使用标准库的大小写转换函数。
相关文章

原文

Some internet standards only support ASCII characters, but the world uses much more than the Latin alphabet. Thus, a mapping from Unicode to ASCII for use in domain names is required.

NamePrep was part of that solution, defined in RFC 3491 as a profile of StringPrep, and is crucially a component of Internationalizing Domain Names in Applications (IDNA), also known as “IDNA 2003”. The StringPrep algorithm is defined in RFC 3454. IDNA 2003 has been obsoleted by IDNA 2008 defined in RFC 5890, 5891, 5892, and 5893.

Python supports IDNA 2003 through the idna codec (str.encode('idna')) and IDNA 2008 is supported by the idna package on the Python package Index. Python's implementation of StringPrep is implemented in the stringprep module in the standard library. In general, you should be using the idna package (IDNA 2008) and not .encode("idna") (IDNA 2003), but sometimes you do need the older behavior.

StringPrep defines the “case folding” step (case folding is approximately “how to lowercase/uppercase a codepoint”) in Section 3.2, enabling case-insensitive comparisons of strings, by mapping all characters through mapping tables B.2 and B.3. B.2 is effectively str.lower(), lowercasing all characters according to Unicode rules and B.3 contains the exceptions. The Python code implementing this (and assuming B.3 table is captured correctly) is the following code below:

def map_table_b3(code):
    r = b3_exceptions.get(ord(code))
    if r is not None: return r
    return code.lower()

And that might seem fine... and the title probably gave it away already. The str.lower() call in this function is a vulnerability!

Why? Because str uses whatever Unicode data that the particular Python interpreter is shipped with, you can figure out what Unicode version your Python interpreter uses by accessing unicodedata.unidata_version:

>>> import unicodedata
>>> unicodedata.unidata_version
'17.0.0'

There's also a database of Unicode 3.2.0 data available on every version of Python (unicodedata.ucd_3_2_0) specifically for the StringPrep and IDNA algorithms:

$ grep -I "ucd_3_2_0" -R Lib/
Lib/stringprep.py:from unicodedata import ucd_3_2_0 as unicodedata
Lib/encodings/idna.py:from unicodedata import ucd_3_2_0 as unicodedata

This is important! StringPrep depends on this specific version of Unicode to operate consistently, the B.2 and B.3 tables in RFC 3454 are essentially Unicode 3.2.0 case-folding rules encoded into a table. So we need to use Unicode 3.2.0 case-folding rules, not newer Unicode case-folding rules. This is why calling str.lower() represents a difference in the implementation and the specification, and therefore a vulnerability:

# RFC 3454 compliant value ('Ꭰ' is U+13A0)
>>> "ᎠᎠ".encode("idna")
'xn--58da'

# Value if using Unicode 17.0.0 case-folding
>>> "ᎠᎠ".encode("idna")
'xn--kz9aa'

The fix was to create new exceptions so that str.lower() would behave as if it was using Unicode 3.2.0 for only particular function. So, we go through each Unicode codepoint and record when the behavior of str.lower() is different when comparing the Unicode version shipped with Python and Unicode 3.2.0. And that's all, now IDNA 2003 is consistent with the specification.

Thanks to Bitshift for reporting the vulnerability, Stan Ulbrych for co-developing the remediation, and Marc-Andre Lemburg and Petr Viktorin for reviewing the remediation. See CVE-2026-17084 for more details.

My work as the Security Developer-in-Residence at the Python Software Foundation is sponsored by Alpha-Omega. Thanks to Alpha-Omega for supporting security in the Python ecosystem.

Wow, you made it to the end!

联系我们 contact @ memedata.com