mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-22 21:44:15 +02:00
Resovle endcoding issue
This commit is contained in:
+34
-3
@@ -15,10 +15,41 @@ function getSegmentRegex(patternPart: string): RegExp {
|
||||
return regex;
|
||||
}
|
||||
|
||||
// Decodes percent-encoding (so an encoded slash like `%2F` is treated as a
|
||||
// real path separator, matching what most backends will do) and then
|
||||
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
|
||||
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment
|
||||
// or a wildcard-swallowed sequence under `/public/*`.
|
||||
function decodeAndResolvePath(p: string): string[] {
|
||||
const rawParts = p.split("/").filter(Boolean);
|
||||
|
||||
const resolved: string[] = [];
|
||||
for (const rawPart of rawParts) {
|
||||
let part: string;
|
||||
try {
|
||||
part = decodeURIComponent(rawPart);
|
||||
} catch {
|
||||
part = rawPart;
|
||||
}
|
||||
|
||||
// an encoded slash can turn one raw segment into several real ones
|
||||
for (const segment of part.split("/").filter(Boolean)) {
|
||||
if (segment === ".") {
|
||||
continue;
|
||||
} else if (segment === "..") {
|
||||
resolved.pop();
|
||||
} else {
|
||||
resolved.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
const patternParts = pattern.split("/").filter(Boolean);
|
||||
const pathParts = decodeAndResolvePath(path);
|
||||
|
||||
function matchSegments(
|
||||
patternIndex: number,
|
||||
|
||||
@@ -236,6 +236,38 @@ function runTests() {
|
||||
"Root path should not match non-root path"
|
||||
);
|
||||
|
||||
// Path traversal / encoded-slash bypass regression tests
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public/../admin"),
|
||||
false,
|
||||
"Literal .. traversal out of an allowed prefix must not match"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2F..%2Fadmin"),
|
||||
false,
|
||||
"Encoded-slash traversal out of an allowed prefix must not match"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2f..%2fadmin%2ffile"),
|
||||
false,
|
||||
"Encoded-slash traversal is case-insensitively decoded before matching"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("admin/*", "public/../admin/secret"),
|
||||
true,
|
||||
".. traversal INTO a restricted path should still be caught by its own rule"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public%2Ffoo"),
|
||||
true,
|
||||
"Encoded slash without traversal should still resolve and match normally"
|
||||
);
|
||||
assertEquals(
|
||||
isPathAllowed("public/*", "public/./foo"),
|
||||
true,
|
||||
"Single-dot segments are a no-op and should not affect matching"
|
||||
);
|
||||
|
||||
console.log("All path matching tests passed!");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user