diff --git a/server/lib/pathMatch.ts b/server/lib/pathMatch.ts index 2c3444b24..ebc03783a 100644 --- a/server/lib/pathMatch.ts +++ b/server/lib/pathMatch.ts @@ -1,5 +1,3 @@ -const MAX_RECURSION_DEPTH = 100; - const segmentRegexCache = new Map(); 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(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]; }