你的 Java Regex 可能被武器化(以及如何阻止)
Source: Dev.to
大多数开发者并未意识到他们的输入验证是一种随时可能发生的拒绝服务(DoS)漏洞。让我来给你展示一下我的意思。
String regex = "^([a-zA-Z0-9]+)+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,}$";
Pattern.compile(regex).matcher(input).matches();
看起来没问题,对吧?现在给它输入以下内容:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
你的 CPU 立刻飙到 100 % 并保持不动。这被称为 ReDoS(正则表达式拒绝服务),其原因是 Java 的正则引擎使用回溯。当匹配以特定方式失败时,某些模式会导致指数级的时间复杂度。
攻击者已经知道这点。他们向你的验证端点发送精心构造的输入,观察你的服务器被熔化。
回溯问题
Java 的 java.util.regex 使用带回溯的 NFA(非确定性有限自动机)。当匹配在中途失败时,引擎会回溯并尝试其他路径。对于像 ([a-zA-Z0-9]+)+ 这样的嵌套量词,路径数量会呈指数级爆炸。
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa! 这段输入有 33 个字符。正则引擎将在放弃前尝试大约 2³³ 种组合——约 80 亿次操作,仅一次验证调用就如此。
解决方案
有一种由 Google 构建的不同类型的正则引擎,叫做 RE2。它使用 DFA(确定性有限自动机),保证线性时间匹配。无论模式或输入多么恶意,匹配时间始终是 O(n),其中 n 为输入长度。
我一直在开发一个名为 Rules 的 Java 验证库,它捆绑了 RE2J(RE2 的 Java 移植)的已修补分支。该分支包含了对原实现中发现的一些漏洞的修复。
Rule email = StringRules.matches(
"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
);
ValidationResult result = email.validate(userInput);
if (result.isValid()) {
// safe to use
}
相同的模式,但现在它无法被武器化。底层的 RE2J 引擎根本不进行回溯,因此不会出现灾难性的回溯。
还能出什么问题
HashDoS
攻击者可以构造所有哈希到同一桶的键,将你的 O(1) HashMap 查找变成 O(n)。该库提供了 SecureHashMap,使用带随机密钥的 SipHash‑2‑4 来防止此类攻击。
Timing Attacks
使用 equals() 比较机密信息会通过时间差泄露信息。逐字符比较在不匹配时会提前退出,使攻击者能够一次猜测一个字符。该库提供了常量时间比较函数。
Stack Overflow via Recursion
自引用的数据结构在验证过程中可能导致栈溢出。该库会跟踪深度并检测循环。
快速示例
使用组合的多个规则验证用户注册:
Rule username = Rules.all(
StringRules.notBlank(),
StringRules.lengthBetween(3, 20),
StringRules.matches("^[a-zA-Z0-9_]+$")
);
Rule password = Rules.all(
StringRules.minLength(12),
StringRules.matches("[A-Z]"),
StringRules.matches("[a-z]"),
StringRules.matches("[0-9]")
);
username.validate("user_dev").isValid(); // true
password.validate("SecurePass123").isValid(); // true
获取方式
Maven
io.github.xoifaii
rules
1.0.0
或者从 GitHub 仓库 克隆下来,如果你想自行探索。完整的 API 文档在 wiki 中。RE2J 分支已捆绑,且没有任何外部依赖。
如果你有面向公众的 Java 应用使用正则进行输入验证,值得检查你的模式是否存在漏洞。像 recheck 这样的工具可以分析模式是否存在 ReDoS,非常有用。
或者直接使用一种从根本上消除该问题的引擎。