mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-13 16:19:06 +02:00
fix(server): avoid exponential backtracking in isPathAllowed (#3862)
This commit is contained in:
+32
-50
@@ -1,5 +1,3 @@
|
||||
const MAX_RECURSION_DEPTH = 100;
|
||||
|
||||
const segmentRegexCache = new Map<string, RegExp>();
|
||||
|
||||
function getSegmentRegex(patternPart: string): RegExp {
|
||||
@@ -56,55 +54,39 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
const patternParts = decodeAndResolvePath(pattern);
|
||||
const pathParts = decodeAndResolvePath(path);
|
||||
|
||||
function matchSegments(
|
||||
patternIndex: number,
|
||||
pathIndex: number,
|
||||
depth: number = 0
|
||||
): boolean {
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
return false;
|
||||
}
|
||||
// Iterative dynamic-programming matcher. The previous recursive
|
||||
// implementation forked on every bare `*`, which backtracked
|
||||
// exponentially and let an unauthenticated path rule pin the event loop.
|
||||
// `dp[i][j]` is true when patternParts[i..] matches pathParts[j..].
|
||||
const m = patternParts.length;
|
||||
const n = pathParts.length;
|
||||
const dp: boolean[][] = Array.from({ length: m + 1 }, () =>
|
||||
new Array<boolean>(n + 1).fill(false)
|
||||
);
|
||||
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
if (patternIndex >= patternParts.length) {
|
||||
return pathIndex >= pathParts.length;
|
||||
}
|
||||
|
||||
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);
|
||||
dp[m][n] = true;
|
||||
for (let j = 0; j < n; j++) {
|
||||
dp[m][j] = false;
|
||||
}
|
||||
for (let i = m - 1; i >= 0; i--) {
|
||||
dp[i][n] = patternParts.slice(i).every((p) => p === "*");
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user