fix(server): avoid exponential backtracking in isPathAllowed (#3862)

This commit is contained in:
Tyagiquamar
2026-10-12 18:18:07 +05:30
parent 58a3701fa8
commit ee00ed5a65
+32 -50
View File
@@ -1,5 +1,3 @@
const MAX_RECURSION_DEPTH = 100;
const segmentRegexCache = new Map<string, RegExp>(); const segmentRegexCache = new Map<string, RegExp>();
function getSegmentRegex(patternPart: string): RegExp { function getSegmentRegex(patternPart: string): RegExp {
@@ -56,55 +54,39 @@ export function isPathAllowed(pattern: string, path: string): boolean {
const patternParts = decodeAndResolvePath(pattern); const patternParts = decodeAndResolvePath(pattern);
const pathParts = decodeAndResolvePath(path); const pathParts = decodeAndResolvePath(path);
function matchSegments( // Iterative dynamic-programming matcher. The previous recursive
patternIndex: number, // implementation forked on every bare `*`, which backtracked
pathIndex: number, // exponentially and let an unauthenticated path rule pin the event loop.
depth: number = 0 // `dp[i][j]` is true when patternParts[i..] matches pathParts[j..].
): boolean { const m = patternParts.length;
if (depth > MAX_RECURSION_DEPTH) { const n = pathParts.length;
return false; const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
} new Array<boolean>(n + 1).fill(false)
);
const currentPatternPart = patternParts[patternIndex]; dp[m][n] = true;
const currentPathPart = pathParts[pathIndex]; for (let j = 0; j < n; j++) {
dp[m][j] = false;
if (patternIndex >= patternParts.length) { }
return pathIndex >= pathParts.length; for (let i = m - 1; i >= 0; i--) {
} dp[i][n] = patternParts.slice(i).every((p) => p === "*");
if (pathIndex >= pathParts.length) {
return patternParts.slice(patternIndex).every((p) => p === "*");
}
if (currentPatternPart === "*") {
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
return true;
}
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
return true;
}
return false;
}
if (currentPatternPart.includes("*")) {
const regex = getSegmentRegex(currentPatternPart);
if (regex.test(currentPathPart)) {
return matchSegments(
patternIndex + 1,
pathIndex + 1,
depth + 1
);
}
return false;
}
if (currentPatternPart !== currentPathPart) {
return false;
}
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
} }
return matchSegments(0, 0, 0); for (let i = m - 1; i >= 0; i--) {
const currentPatternPart = patternParts[i];
for (let j = n - 1; j >= 0; j--) {
if (currentPatternPart === "*") {
// match zero segments, or consume one path segment
dp[i][j] = dp[i + 1][j] || dp[i][j + 1];
} else if (currentPatternPart.includes("*")) {
const regex = getSegmentRegex(currentPatternPart);
dp[i][j] = regex.test(pathParts[j]) && dp[i + 1][j + 1];
} else {
dp[i][j] =
currentPatternPart === pathParts[j] && dp[i + 1][j + 1];
}
}
}
return dp[0][0];
} }