真正危险的 regexp 字面量需结合 ast 上下文、字符串内容及作用域引用三重判断:出现在 newexpression(regexp)、敏感调用链赋值、含未校验用户输入等场景;静态定义如 const pattern = /d+/g 通常安全,除非被二次动态构造。

直接用 acorn 或 @babel/parser 解析完就遍历 RegExpLiteral 节点然后替换,大概率会破坏运行时行为——正则的安全风险不在字面量本身,而在其构造方式、动态拼接逻辑、以及是否被用于不安全上下文(如 eval、new Function、location.href 注入)。必须结合 AST 上下文 + 字符串字面量内容 + 作用域引用三重判断,才能准确识别“该改”和“不该动”的正则。
怎么定位真正危险的 RegExp 字面量
不是所有带 /.../ 的节点都该被重写。关键看它是否出现在高风险构造路径中:
-
RegExpLiteral出现在NewExpression中且callee.name === 'RegExp',且第一个参数是字符串字面量(StringLiteral)或模板字面量(TemplateLiteral),这类极易被绕过静态检测 -
RegExpLiteral被赋值给变量后,该变量又被传入eval、setTimeout、setInterval、location.replace等敏感调用链 -
RegExp构造函数参数中包含未校验的用户输入(如req.query.id、document.cookie),需向上追溯CallExpression的arguments是否含MemberExpression或Identifier引用外部数据源 - 跳过
const PATTERN = /\d+/g这类纯静态定义——除非它后续被new RegExp(PATTERN.source)二次构造并拼接
为什么不能只靠正则字符串内容做安全判定
单纯扫描 /.*?/i 或 /[a-z]+/ 这类模式毫无意义。真正的问题往往藏在上下文里:
-
new RegExp(userInput + '$'):AST 中userInput是一个Identifier,它的来源可能来自URLSearchParams或localStorage,必须沿ReferencePath向上查到声明处或调用入口 -
const r = /\w+/; location.href = `/?q=${r}`:这里r本身安全,但字符串化后参与 URL 拼接,触发 XSS,需检测BinaryExpression右侧是否为TemplateLiteral且含location、document等全局对象 -
eval(`/${unsafe}/g`):AST 中eval是CallExpression,其第一个argument是TemplateLiteral,内部插槽含Identifier,此时必须标记整个表达式为高危,而非仅修改unsafe变量名
重写策略必须区分构造方式与执行环境
不同正则使用场景,修复手段完全不同,硬统一替换会引入新 bug:
- 对纯字面量
/\d{3}-\d{2}-\d{4}/,可安全替换成预编译的const SSN_REGEX = new RegExp('\\d{3}-\\d{2}-\\d{4}', 'g'),并加/* @__PURE__ */注释避免 tree-shaking 误删 - 对动态构造
new RegExp(prefix + '\d+'),不能直接改字符串,而应插入校验:替换为new RegExp(escapeRegExp(prefix) + '\\d+', 'g'),其中escapeRegExp需提前注入或要求项目已存在 - 对用于
replace的正则(如str.replace(//g, '<')),若原模式无捕获组,可转成字符串方法调用:str.split(',避免正则引擎开销与回溯风险 - 所有重写必须保留原始
flags(g、i、m等),且生成新节点时用t.newExpression(t.identifier('RegExp'), [stringLiteralNode, flagLiteralNode]),不能漏掉flags参数导致行为变更
容易被忽略的边界:跨文件引用与模块导出
一个正则定义在 utils/regex.js 中导出,被十个文件 import 使用,只改定义文件远远不够:
- 若该正则被
export const EMAIL = /\S+@\S+\.\S+/导出,则重写时必须同步更新所有import { EMAIL }处的调用上下文——尤其是它是否出现在new Function(...EMAIL...)中 - ESM 动态
import()加载的模块内正则无法被单文件 AST 分析覆盖,需配合打包器插件(如esbuild.onLoad)做全图扫描 - CommonJS 的
module.exports = { pattern: /.../ }会被 Babel 转成exports.pattern = /.../,此时pattern是属性访问,需识别MemberExpression的object.name === 'exports'才能准确定位 - 没处理
require.resolve或import.meta.url构造的路径拼接正则,会导致混淆后路径失效,这类必须人工标注/* @no-rewrite-regex */注释跳过










