From 64ae230d236df67282e507e41e61ef99932068ee Mon Sep 17 00:00:00 2001 From: breken Date: Sun, 13 Sep 2026 19:00:59 -0700 Subject: [PATCH] fix(rules): decode percent-encoded PATH rule patterns before matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/lib/pathMatch.ts | 7 ++++- server/routers/badger/verifySession.test.ts | 33 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/server/lib/pathMatch.ts b/server/lib/pathMatch.ts index 8ef579607..2c3444b24 100644 --- a/server/lib/pathMatch.ts +++ b/server/lib/pathMatch.ts @@ -20,6 +20,11 @@ function getSegmentRegex(patternPart: string): RegExp { // 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/*`. +// +// 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[] { const rawParts = p.split("/").filter(Boolean); @@ -48,7 +53,7 @@ function decodeAndResolvePath(p: string): string[] { } export function isPathAllowed(pattern: string, path: string): boolean { - const patternParts = pattern.split("/").filter(Boolean); + const patternParts = decodeAndResolvePath(pattern); const pathParts = decodeAndResolvePath(path); function matchSegments( diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index c91805f8e..6254de025 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -386,6 +386,38 @@ function runSpecialCharacterTests() { 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() { console.log("\nRunning isIpInRegion tests..."); @@ -446,6 +478,7 @@ function runRegionTests() { try { runTests(); runSpecialCharacterTests(); + runEncodedPatternTests(); runRegionTests(); console.log("\n✅ All tests passed!"); } catch (error) {