功能说明
正则表达式在匹配嵌套量词时可能发生灾难性回溯,输入每增加一个字符,耗时呈指数增长。若模式来自用户配置或管理后台,攻击者可以用一条看似普通的正则和一段精心构造的字符串占满 CPU。Python 标准库 re 没有超时参数,本例使用第三方 regex 模块,它在 fullmatch、match 等调用上支持 timeout 秒数,超时抛出 TimeoutError。
仅依赖超时也不够,因此函数还限制输入长度,避免在进入匹配前就消耗大量内存。异常被包装成带有明确语义的 TimeoutError,调用方可以据此返回四百错误或降级到字面量匹配。main 分别演示正常匹配、超长拒绝和灾难性回溯超时。
关键代码
USER_PATTERN 预编译一次,避免每次请求重复解析。safe_fullmatch 先检查字符数,再调用 pattern.fullmatch 并传入 timeout。捕获 TimeoutError 后重新抛出,保留原因链,便于日志记录。主程序用 a 加重复的嵌套量词构造经典回溯模式,输入四千个 a 再加一个 b,刻意设置一毫秒预算。正常路径使用命名分组提取用户名和域名。
使用注意
regex 是第三方依赖,部署时需加入依赖清单并锁定版本。超时时间应结合接口延迟预算设置,过长失去保护意义,过短会误杀正常复杂模式。正则引擎在超时后需要一定时间清理内部状态,极高并发下仍可能占用资源。不要允许用户直接提交任意正则;应使用白名单模式,或改用 RE2 等线性时间引擎。长度限制按字节还是字符计算要明确,多字节输入应按编码后的字节数限制。
验证方式
运行脚本,第一行输出 ada_lovelace 与 example.com。长度检查输出 rejected 信息,说明五千字符在匹配前被拒绝。恶意模式在约一毫秒后输出 timeout 信息,证明灾难性回溯被预算截断。可把 timeout 调大后再次运行,观察耗时随输入长度急剧上升。
代码示例
import regex
USER_PATTERN = regex.compile(
r"^(?P<user>[a-z0-9_]{3,32})@(?P<domain>[a-z0-9.-]{1,120})$"
)
def safe_fullmatch(
pattern: regex.Pattern,
text: str,
*,
timeout: float = 0.05,
max_length: int = 4096,
) -> regex.Match | None:
if len(text) > max_length:
raise ValueError(f"input length {len(text)} exceeds {max_length}")
try:
return pattern.fullmatch(text, timeout=timeout)
except TimeoutError as error:
raise TimeoutError("pattern evaluation exceeded the time budget") from error
if __name__ == "__main__":
match = safe_fullmatch(USER_PATTERN, "[email protected]")
assert match is not None
print(match.group("user"), match.group("domain"))
try:
safe_fullmatch(USER_PATTERN, "x" * 5000)
except ValueError as error:
print("rejected:", error)
evil = regex.compile(r"^(a+)+$")
try:
safe_fullmatch(evil, "a" * 4000 + "b", timeout=0.001)
except TimeoutError as error:
print("timeout:", error)