10. Security Considerations (安全考虑)
UTF-8的实现者需要考虑如何处理非法UTF-8序列的安全方面。在某些情况下,攻击者可以通过向其发送UTF-8语法不允许的八位字节序列来利用不谨慎的UTF-8解析器。
主要安全威胁
1. 过长编码攻击 (Overlong Encoding Attack)
这是一种特别微妙的攻击形式,可以针对以下解析器进行:
- 对UTF-8编码形式的输入执行安全关键的有效性检查
- 但将某些非法八位字节序列解释为字符
攻击示例1: NUL字符绕过
正常禁止: 00 (NUL字符)
解析器正确拒绝: 00
过长编码攻击: C0 80 (非法的2字节编码)
易受攻击的解析器错误地将其解释为: NUL字符
危险: 绕过了安全检查!
场景:
- 解析器可能禁止编码为单八位字节序列
00的NUL字符 - 但错误地允许非法的两八位字节序列
C0 80并将其解释为NUL字符
攻击示例2: 路径遍历攻击 (实际发生过!)
正常禁止: 2F 2E 2E 2F ("/../")
解析器正确拒绝: /../
过长编码攻击: 2F C0 AE 2E 2F
/ ..非法.. ./
易受攻击的解析器错误地将其解释为: /../
危险: 目录遍历攻击成功!
真实案例:
- 这种利用实际上已在2001年攻击Web服务器的广泛传播的病毒中使用
- 安全威胁是真实存在的!
详细分析
字符: '.'
合法编码: 2E (1字节)
过长编码: C0 AE (2字节,非法)
E0 80 AE (3字节,非法)
F0 80 80 AE (4字节,非法)
攻击向量:
输入: /etc/passwd
绕过检查: /..C0AE./..C0AE./etc/passwd
如果解析器不正确处理: 成功访问 /../../../etc/passwd
2. 缓冲区溢出风险
编码为UTF-8时的另一个安全问题:
历史遗留问题
ISO/IEC 10646的UTF-8描述允许编码字符编号高达 U+7FFFFFFF,产生最多6字节的序列。
风险
因此,如果以下情况,存在缓冲区溢出的风险:
- 字符编号的范围没有明确限制为 U+10FFFF
- 缓冲区大小没有考虑5字节和6字节序列的可能性
正确的限制
有效Unicode范围: U+0000 到 U+10FFFF
最大字节数: 4字节
无效范围: U+110000及以上
防御措施: 严格验证字符范围,拒绝超过U+10FFFF的编码。
3. 规范化问题
字符编码的一个特征(包括UTF-8)也可能影响安全性:
多重表示问题
"同一事物" (就用户而言)可以由几个不同的字符序列表示。
示例:带重音的e
方式1: 预组合字符
U+00E9 (E ACUTE, é)
UTF-8: C3 A9
方式2: 规范等价序列
U+0065 U+0301 (E + COMBINING ACUTE)
UTF-8: 65 CC 81
用户看到的: é (完全相同)
字节序列: 完全不同
安全影响
即使UTF-8为每个字符序列提供单一的字节序列,"同一事物"存在多个字符序列的事实在涉及以下操作时可能产生安全后果:
- 字符串匹配 (String matching)
- 索引 (Indexing)
- 搜索 (Searching)
- 排序 (Sorting)
- 正则表达式匹配 (Regular expression matching)
- 选择 (Selection)
安全示例
场景: 访问控制
凭证中的标识符: "café" (使用U+00E9)
访问控制列表: "café" (使用U+0065 + U+0301)
问题: 字节级别不匹配!
结果: 访问被拒绝(或被授予,取决于实现)
安全风险: 认证/授权绕过
解决方案:Unicode规范化
使用Unicode规范化形式 [UAX15]:
规范化形式:
- NFC (Normalization Form Canonical Composition)
- NFD (Normalization Form Canonical Decomposition)
- NFKC (Normalization Form Compatibility Composition)
- NFKD (Normalization Form Compatibility Decomposition)
推荐: 在安全关键操作前进行规范化
安全最佳实践
1. 严格验证
def validate_utf8(data):
"""严格的UTF-8验证"""
# 1. 检查字节序列语法
if not is_valid_syntax(data):
raise InvalidUTF8Error("Invalid byte sequence")
# 2. 检查过长编码
if has_overlong_encoding(data):
raise InvalidUTF8Error("Overlong encoding detected")
# 3. 检查代理对
if has_surrogates(data):
raise InvalidUTF8Error("Surrogate pairs not allowed")
# 4. 检查范围
if exceeds_unicode_range(data):
raise InvalidUTF8Error("Character exceeds U+10FFFF")
return True
2. 拒绝非法序列
必须 (MUST) 拒绝:
✗ 过长编码 (C0 80 for NUL)
✗ 代理对 (ED A0 80)
✗ 超出范围 (F4 90 80 80)
✗ 截断序列 (E4 BD)
✗ 孤立的后续字节 (80)
✗ 无效的首字节 (C0, C1, F5-FF)
3. 规范化
import unicodedata
def secure_compare(str1, str2):
"""安全的字符串比较"""
# 规范化为NFC形式
norm1 = unicodedata.normalize('NFC', str1)
norm2 = unicodedata.normalize('NFC', str2)
return norm1 == norm2
4. 限制缓冲区
// C语言示例
#define MAX_UTF8_CHAR_LEN 4 // 不是6!
char buffer[MAX_UTF8_CHAR_LEN * MAX_CHARS + 1];
安全检查清单
| 检查项 | 重要性 | 说明 |
|---|---|---|
| ✅ 拒绝过长编码 | ⭐⭐⭐⭐⭐ | 防止安全绕过 |
| ✅ 验证字符范围 | ⭐⭐⭐⭐⭐ | 限制为U+0000-U+10FFFF |
| ✅ 拒绝代理对 | ⭐⭐⭐⭐ | U+D800-U+DFFF无效 |
| ✅ 检测截断序列 | ⭐⭐⭐⭐ | 防止缓冲区读取错误 |
| ✅ Unicode规范化 | ⭐⭐⭐⭐ | 安全关键字符串比较 |
| ✅ 缓冲区大小限制 | ⭐⭐⭐ | 防止溢出 |
真实世界的攻击示例
Code Red II 蠕虫 (2001)
攻击: 使用过长UTF-8编码绕过IIS路径检查
编码: ..%c0%af..%c0%af..%c0%af
等同于: ../../..
影响: 数百万Web服务器被感染
教训
永远不要信任输入!
- 始终严格验证UTF-8序列
- 拒绝所有非规范编码
- 在安全关键操作前进行规范化