← 中文资源

简体中文版本

不可信输入的正则超时保护

使用 regex 模块的超时参数、输入长度上限和显式异常处理,限制用户提交正则造成的灾难性回溯。

阅读英文原文

功能说明

正则表达式在匹配嵌套量词时可能发生灾难性回溯,输入每增加一个字符,耗时呈指数增长。若模式来自用户配置或管理后台,攻击者可以用一条看似普通的正则和一段精心构造的字符串占满 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)