fix(rules): decode percent-encoded PATH rule patterns before matching

isPathAllowed decodes the incoming request path (and Badger already sends
Go's decoded req.URL.Path), but compared it against the rule pattern as raw
text. isValidUrlGlobPattern rejects raw spaces and non-ASCII and only accepts
them percent-encoded, so a PATH rule such as `/my%20docs/*` or `/caf%C3%A9`
was stored as `my%20docs` / `caf%C3%A9` and compared against `my docs` /
`café`, and could therefore never match any request.

Run the pattern through the same decodeAndResolvePath normalisation as the
request path so both sides are compared in decoded form.

Claude-Session: https://claude.ai/code/session_0134ujLF81GyXsCByibLcYsz
This commit is contained in:
breken
2026-09-13 19:00:59 -07:00
parent 17375348b0
commit 64ae230d23
2 changed files with 39 additions and 1 deletions
+6 -1
View File
@@ -20,6 +20,11 @@ function getSegmentRegex(patternPart: string): RegExp {
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/` // resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment // or `/public/../admin/` is matched as `/admin/`, not as a literal segment
// or a wildcard-swallowed sequence under `/public/*`. // or a wildcard-swallowed sequence under `/public/*`.
//
// Applied to both the request path and the rule pattern: the pattern
// validator only accepts spaces / non-ASCII in percent-encoded form, so a
// rule like `/my%20docs/*` must be compared against the decoded segment
// `my docs`, not the literal text `my%20docs`.
function decodeAndResolvePath(p: string): string[] { function decodeAndResolvePath(p: string): string[] {
const rawParts = p.split("/").filter(Boolean); const rawParts = p.split("/").filter(Boolean);
@@ -48,7 +53,7 @@ function decodeAndResolvePath(p: string): string[] {
} }
export function isPathAllowed(pattern: string, path: string): boolean { export function isPathAllowed(pattern: string, path: string): boolean {
const patternParts = pattern.split("/").filter(Boolean); const patternParts = decodeAndResolvePath(pattern);
const pathParts = decodeAndResolvePath(path); const pathParts = decodeAndResolvePath(path);
function matchSegments( function matchSegments(
@@ -386,6 +386,38 @@ function runSpecialCharacterTests() {
console.log("All special character tests passed!"); console.log("All special character tests passed!");
} }
function runEncodedPatternTests() {
console.log("\nRunning percent-encoded pattern tests...");
// isValidUrlGlobPattern accepts percent-encoded sequences and rejects
// raw spaces / non-ASCII, so `%20` and `%C3%A9` are the only way to write
// a PATH rule for such a path. Badger sends the request path already
// decoded (Go's req.URL.Path), and isPathAllowed decodes it again, so the
// rule pattern must be decoded the same way or it can never match.
assertEquals(
isPathAllowed("/my%20docs/*", "/my docs/report.pdf"),
true,
"Percent-encoded space in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my%20docs/report.pdf"),
true,
"Percent-encoded space in pattern should match raw-encoded request path"
);
assertEquals(
isPathAllowed("/caf%C3%A9", "/café"),
true,
"Percent-encoded UTF-8 in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my-docs/report.pdf"),
false,
"Decoded pattern must still reject a different path"
);
console.log("All percent-encoded pattern tests passed!");
}
function runRegionTests() { function runRegionTests() {
console.log("\nRunning isIpInRegion tests..."); console.log("\nRunning isIpInRegion tests...");
@@ -446,6 +478,7 @@ function runRegionTests() {
try { try {
runTests(); runTests();
runSpecialCharacterTests(); runSpecialCharacterTests();
runEncodedPatternTests();
runRegionTests(); runRegionTests();
console.log("\n✅ All tests passed!"); console.log("\n✅ All tests passed!");
} catch (error) { } catch (error) {