Skip to content

Commit 65b714e

Browse files
authored
fix(shared): normalize URL paths in createPathMatcher [v2] (#8312)
1 parent 93acb6f commit 65b714e

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

‎packages/shared/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@clerk/shared",
3-
"version": "2.22.0",
3+
"version": "2.22.1",
44
"description": "Internal package utils used by the Clerk SDKs",
55
"repository": {
66
"type": "git",

‎packages/shared/src/pathMatcher.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,49 @@ export type WithPathPatternWildcard<T = string> = `${T & string}(.*)`;
66
export type PathPattern = Autocomplete<WithPathPatternWildcard>;
77
export type PathMatcherParam = Array<RegExp | PathPattern> | RegExp | PathPattern;
88

9+
export class MalformedURLError extends Error {
10+
public readonly statusCode = 400;
11+
public readonly cause?: unknown;
12+
13+
constructor(pathname: string, cause?: unknown) {
14+
super(`Malformed encoding in URL path: ${pathname}`);
15+
this.name = 'MalformedURLError';
16+
this.cause = cause;
17+
}
18+
}
19+
20+
/**
21+
* String-based check for MalformedURLError that works across package bundles
22+
* where `instanceof` would fail due to duplicate class identities.
23+
*/
24+
export function isMalformedURLError(e: unknown): e is MalformedURLError {
25+
return e instanceof Error && e.name === 'MalformedURLError';
26+
}
27+
928
const precomputePathRegex = (patterns: Array<string | RegExp>) => {
1029
return patterns.map(pattern => (pattern instanceof RegExp ? pattern : pathToRegexp(pattern)));
1130
};
1231

32+
/**
33+
* Normalizes a URL path for safe route matching.
34+
*
35+
* 1. Decodes percent-encoded unreserved characters using decodeURI (not
36+
* decodeURIComponent) so path-reserved delimiters like %2F, %3F, %23
37+
* are preserved — matching how framework routers interpret paths.
38+
* 2. Collapses consecutive slashes (e.g. //api/admin → /api/admin) to
39+
* prevent bypass via extra slashes.
40+
*
41+
* @throws {MalformedURLError} if the path contains invalid percent-encoding
42+
*/
43+
export const normalizePath = (pathname: string): string => {
44+
try {
45+
pathname = decodeURI(pathname);
46+
} catch (e) {
47+
throw new MalformedURLError(pathname, e);
48+
}
49+
return pathname.replace(/\/\/+/g, '/');
50+
};
51+
1352
/**
1453
* Creates a function that matches paths against a set of patterns.
1554
*
@@ -19,5 +58,5 @@ const precomputePathRegex = (patterns: Array<string | RegExp>) => {
1958
export const createPathMatcher = (patterns: PathMatcherParam) => {
2059
const routePatterns = [patterns || ''].flat().filter(Boolean);
2160
const matchers = precomputePathRegex(routePatterns);
22-
return (pathname: string) => matchers.some(matcher => matcher.test(pathname));
61+
return (pathname: string) => matchers.some(matcher => matcher.test(normalizePath(pathname)));
2362
};

0 commit comments

Comments
 (0)