- failure actual 截断格式改为 …(共 N 字符),标量不序列化直接返回 - 新增 redos.ts 实现 ReDoS 静态检测(嵌套量词/重叠交替),启动期拒绝危险正则 - JSON body rules 共享同一次 JSON.parse 结果,避免重复解析 - checkCssRule 重构为线性流程,消除 exist:true 与无 operator 的冗余分支 - extract checkEarlyTimeout 辅助函数,明确提前 duration 检查意图 - 补充 303/307/308 重定向、相对路径 Location、混合 body rules 集成测试
152 lines
4.0 KiB
TypeScript
152 lines
4.0 KiB
TypeScript
export function isUnsafeRegex(pattern: string): boolean {
|
|
const groups = findQuantifiedGroups(pattern);
|
|
return groups.some((group) => containsQuantifier(group) || containsOverlappingAlternation(group));
|
|
}
|
|
|
|
function containsOverlappingAlternation(pattern: string): boolean {
|
|
const branches = splitTopLevelAlternation(stripGroupPrefix(pattern));
|
|
if (branches.length < 2) return false;
|
|
|
|
for (let i = 0; i < branches.length; i++) {
|
|
const current = branches[i]!;
|
|
if (current === "") continue;
|
|
for (let j = i + 1; j < branches.length; j++) {
|
|
const next = branches[j]!;
|
|
if (next === "") continue;
|
|
if (current === next || current.startsWith(next) || next.startsWith(current)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function containsQuantifier(pattern: string): boolean {
|
|
const input = stripGroupPrefix(pattern);
|
|
let inCharClass = false;
|
|
|
|
for (let i = 0; i < input.length; i++) {
|
|
const char = input[i]!;
|
|
if (isEscaped(input, i)) continue;
|
|
if (char === "[") {
|
|
inCharClass = true;
|
|
continue;
|
|
}
|
|
if (char === "]") {
|
|
inCharClass = false;
|
|
continue;
|
|
}
|
|
if (inCharClass) continue;
|
|
if (char === "*" || char === "+" || char === "?") return true;
|
|
if (char === "{" && readQuantifierBody(input, i) !== null) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function findQuantifiedGroups(pattern: string): string[] {
|
|
const groups: string[] = [];
|
|
const stack: number[] = [];
|
|
let inCharClass = false;
|
|
|
|
for (let i = 0; i < pattern.length; i++) {
|
|
const char = pattern[i]!;
|
|
if (isEscaped(pattern, i)) continue;
|
|
if (char === "[") {
|
|
inCharClass = true;
|
|
continue;
|
|
}
|
|
if (char === "]") {
|
|
inCharClass = false;
|
|
continue;
|
|
}
|
|
if (inCharClass) continue;
|
|
|
|
if (char === "(") {
|
|
stack.push(i);
|
|
continue;
|
|
}
|
|
|
|
if (char === ")") {
|
|
const start = stack.pop();
|
|
if (start === undefined) continue;
|
|
if (hasRepeatingQuantifierAt(pattern, i + 1)) {
|
|
groups.push(pattern.slice(start + 1, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
function hasRepeatingQuantifierAt(pattern: string, index: number): boolean {
|
|
const char = pattern[index];
|
|
if (char === "*" || char === "+") return true;
|
|
if (char !== "{") return false;
|
|
|
|
const body = readQuantifierBody(pattern, index);
|
|
if (body === null) return false;
|
|
const parts = body.split(",");
|
|
if (parts.length === 1) return Number(parts[0]) > 1;
|
|
if (parts[1] === "") return true;
|
|
return Number(parts[1]) > 1;
|
|
}
|
|
|
|
function isEscaped(pattern: string, index: number): boolean {
|
|
let slashCount = 0;
|
|
for (let i = index - 1; i >= 0 && pattern[i] === "\\"; i--) {
|
|
slashCount++;
|
|
}
|
|
return slashCount % 2 === 1;
|
|
}
|
|
|
|
function readQuantifierBody(pattern: string, index: number): null | string {
|
|
const end = pattern.indexOf("}", index + 1);
|
|
if (end === -1) return null;
|
|
|
|
const body = pattern.slice(index + 1, end);
|
|
return /^\d+(?:,\d*)?$/.test(body) ? body : null;
|
|
}
|
|
|
|
function splitTopLevelAlternation(pattern: string): string[] {
|
|
const branches: string[] = [];
|
|
let start = 0;
|
|
let depth = 0;
|
|
let inCharClass = false;
|
|
|
|
for (let i = 0; i < pattern.length; i++) {
|
|
const char = pattern[i]!;
|
|
if (isEscaped(pattern, i)) continue;
|
|
if (char === "[") {
|
|
inCharClass = true;
|
|
continue;
|
|
}
|
|
if (char === "]") {
|
|
inCharClass = false;
|
|
continue;
|
|
}
|
|
if (inCharClass) continue;
|
|
if (char === "(") {
|
|
depth++;
|
|
continue;
|
|
}
|
|
if (char === ")") {
|
|
depth = Math.max(0, depth - 1);
|
|
continue;
|
|
}
|
|
if (char === "|" && depth === 0) {
|
|
branches.push(pattern.slice(start, i));
|
|
start = i + 1;
|
|
}
|
|
}
|
|
|
|
branches.push(pattern.slice(start));
|
|
return branches;
|
|
}
|
|
|
|
function stripGroupPrefix(pattern: string): string {
|
|
if (pattern.startsWith("?:") || pattern.startsWith("?=") || pattern.startsWith("?!")) return pattern.slice(2);
|
|
if (pattern.startsWith("?<=") || pattern.startsWith("?<!")) return pattern.slice(3);
|
|
|
|
const namedCapture = /^\?<[^>]+>/.exec(pattern);
|
|
return namedCapture ? pattern.slice(namedCapture[0].length) : pattern;
|
|
}
|