mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-02 02:30:45 +02:00
Resovle endcoding issue
This commit is contained in:
+34
-3
@@ -15,10 +15,41 @@ function getSegmentRegex(patternPart: string): RegExp {
|
|||||||
return regex;
|
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 {
|
export function isPathAllowed(pattern: string, path: string): boolean {
|
||||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
const patternParts = pattern.split("/").filter(Boolean);
|
||||||
const patternParts = normalize(pattern);
|
const pathParts = decodeAndResolvePath(path);
|
||||||
const pathParts = normalize(path);
|
|
||||||
|
|
||||||
function matchSegments(
|
function matchSegments(
|
||||||
patternIndex: number,
|
patternIndex: number,
|
||||||
|
|||||||
@@ -236,6 +236,38 @@ function runTests() {
|
|||||||
"Root path should not match non-root path"
|
"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!");
|
console.log("All path matching tests passed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user