基于Python unicodedata.category识别与过滤不可见格式控制字符
本文由Gemini辅助撰写,示例代码已在Linux Python 3环境执行验证。
1. 业务背景与潜在风险
在处理用户提交的富文本、文件名、登录凭证或配置内容时,字符串表面呈现的内容与其实际包含的 Unicode 码位可能存在显著差异。现代 Unicode 标准包含大量的不可见字符、排版修饰字符以及双向文本控制字符(BiDi Controls)。例如:
- 零宽空格(Zero Width Space,
\u200b):在视觉呈现上宽度为零,常被用于绕过关键字黑名单过滤,或在两个合法词汇间注入隐藏信息。 - 从右至左覆盖符(Right-to-Left Override,
\u202e):强制改变后续文本的阅读和渲染顺序,攻击者常利用该特性伪造文件扩展名(例如将可执行文件伪装为看似安全的文本文件)。 - 其他 C 类别控制字符(Other Control,
Cc):如 ASCII 码段中的退格符、响铃符、取消符等,可能破坏下游终端渲染、破坏固定格式日志或引发解析层语义错乱。
针对此类不可见字符或排版欺骗攻击,单纯依赖正则表达式的字符集枚举往往难以全面覆盖。Python 标准库 unicodedata 模块提供了访问 Unicode 字符数据库(UCD)的底层接口,能够精确获取每个码位的通用类别(General Category),为构建强健的输入清洗过滤器提供了轻量且标准化的解决方案。
2. Unicode 类别与过滤策略
Unicode 标准为每个已分配码位赋予了一个主类别和子类别标识(由两个字母组成):
- Cf (Other, format):用于控制文本排版和渲染行为的格式控制字符,如零宽连字/非连字、双向流向控制符、软连字符等。绝大部分业务文本输入都不需要保留格式控制符。
- Cc (Other, control):传统控制字符(码位范围为
U+0000至U+001F以及U+007F至U+009F)。在实际业务场景中,除了标准空白控制符(换行符\n、回车符\r、水平制表符\t)之外,其余控制字符通常应被无条件剔除。
通过遍历字符串并调用 unicodedata.category(char),可以精准检出这两类字符,并在清洗文本的同时记录被拦截字符的详细信息(包括类别及其标准 Unicode 名称)。
3. 完整实现与执行验证
以下实现展示了如何通过 unicodedata.category 和 unicodedata.name 构造通用的文本净化清洗器,对潜在的伪造输入进行隔离,并确保中英文字符与合法排版空白符号正常放行:
import unicodedata
def sanitize_text(text: str) -> tuple[str, list[tuple[str, str, str]]]:
cleaned = []
quarantined = []
for char in text:
category = unicodedata.category(char)
# 拦截格式控制字符(Cf)与非标准空白控制字符(Cc)
if category == "Cf" or (category == "Cc" and char not in "\r\n\t"):
try:
char_name = unicodedata.name(char)
except ValueError:
char_name = "<UNKNOWN>"
quarantined.append((char, category, char_name))
else:
cleaned.append(char)
return "".join(cleaned), quarantined
# 模拟包含零宽空格(\u200b)和从右至左覆盖符(\u202e)的视觉欺骗输入
spoofed_input = "admin\u200b\u202eguest.txt"
clean_res, issues = sanitize_text(spoofed_input)
assert clean_res == "adminguest.txt"
assert len(issues) == 2
assert issues[0] == ("\u200b", "Cf", "ZERO WIDTH SPACE")
assert issues[1] == ("\u202e", "Cf", "RIGHT-TO-LEFT OVERRIDE")
# 验证正常中英文字符与合法换行制表符不会被误杀
valid_text = "华为云\tCloud\n2026"
valid_res, valid_issues = sanitize_text(valid_text)
assert valid_res == valid_text
assert len(valid_issues) == 0
print("MATERIAL_OK")
运行上述代码,控制台标准输出打印 MATERIAL_OK,所有断言均验证通过。
4. 关键边界与运维注意事项
在将此类过滤逻辑部署至自动化生产流水线时,需要特别关注以下边界条件与实现细节:
- Python 内置 Unicode 数据库版本依赖:
unicodedata.category的判断依据是 Python 解释器编译时捆绑的 Unicode 字符数据库。在跨版本运行(如从 Python 3.9 迁移至 Python 3.12)时,新版本 Unicode 标准中新增的码位可能从未分配状态(Cn)变为分配状态,需关注极端边缘码位在新旧环境中的判定一致性。 - 未分配与私有保留码位处理:
- 对于未分配码位(
Cn,Other, not assigned)或私有使用区码位(Co,Other, private use),unicodedata.name()会抛出ValueError异常。代码中必须使用try...except ValueError进行兜底防护,避免异常中断整个服务。 - 若安全策略要求更为严苛,除
Cf与Cc外,通常还应考虑将Cn字符一并纳入拒绝名单。
- 对于未分配码位(
- 合法控制字符的业务界定:示例中保留了
\r\n\t,以支持常规的多行文本与结构化排版。然而在单行标识符校验(如用户名、租户 ID、系统文件名)场景下,制表符和换行符同样应当被归入非法控制字符,需依据具体业务接口规范适时调整放行白名单。
5. 官方参考文档
- Python 官方文档 unicodedata 模块:Python 3 官方文档(对应模块与章节)
- 点赞
- 收藏
- 关注作者
评论(0)