Skip to content

Commit c5f8cac

Browse files
authored
fix(astro): normalize URL paths in createRouteMatcher [v1] (#8313)
1 parent 54a9c93 commit c5f8cac

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎packages/astro/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@clerk/astro",
3-
"version": "1.5.6",
3+
"version": "1.5.7",
44
"description": "Clerk SDK for Astro",
55
"keywords": [
66
"auth",

‎packages/astro/src/server/clerk-middleware.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { clerkClient } from './clerk-client';
1515
import { createCurrentUser } from './current-user';
1616
import { getAuth } from './get-auth';
1717
import { getClientSafeEnv, getSafeEnv } from './get-safe-env';
18+
import { isMalformedURLError } from './route-matcher';
1819
import { serverRedirectWithAuth } from './server-redirect-with-auth';
1920
import type {
2021
AstroMiddleware,
@@ -365,6 +366,10 @@ const handleControlFlowErrors = (
365366
requestState: RequestState,
366367
context: AstroMiddlewareContextParam,
367368
): Response => {
369+
if (isMalformedURLError(e)) {
370+
return new Response(null, { status: 400, statusText: 'Bad Request' });
371+
}
372+
368373
switch (e.message) {
369374
case CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN:
370375
return createRedirect({

‎packages/astro/src/server/route-matcher.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,45 @@ type RouteMatcherRoutes = Autocomplete<WithPathPatternWildcard>;
77

88
export type RouteMatcherParam = Array<RegExp | RouteMatcherRoutes> | RegExp | RouteMatcherRoutes;
99

10+
export class MalformedURLError extends Error {
11+
public readonly statusCode = 400;
12+
public readonly cause?: unknown;
13+
14+
constructor(pathname: string, cause?: unknown) {
15+
super(`Malformed encoding in URL path: ${pathname}`);
16+
this.name = 'MalformedURLError';
17+
this.cause = cause;
18+
}
19+
}
20+
21+
/**
22+
* String-based check for MalformedURLError that works across package bundles
23+
* where `instanceof` would fail due to duplicate class identities.
24+
*/
25+
export function isMalformedURLError(e: unknown): e is MalformedURLError {
26+
return e instanceof Error && e.name === 'MalformedURLError';
27+
}
28+
29+
/**
30+
* Normalizes a URL path for safe route matching.
31+
*
32+
* 1. Decodes percent-encoded unreserved characters using decodeURI (not
33+
* decodeURIComponent) so path-reserved delimiters like %2F, %3F, %23
34+
* are preserved — matching how framework routers interpret paths.
35+
* 2. Collapses consecutive slashes (e.g. //api/admin → /api/admin) to
36+
* prevent bypass via extra slashes.
37+
*
38+
* @throws {MalformedURLError} if the path contains invalid percent-encoding
39+
*/
40+
const normalizePath = (pathname: string): string => {
41+
try {
42+
pathname = decodeURI(pathname);
43+
} catch (e) {
44+
throw new MalformedURLError(pathname, e);
45+
}
46+
return pathname.replace(/\/\/+/g, '/');
47+
};
48+
1049
// TODO-SHARED: This can be moved to @clerk/shared as an identical implementation exists in @clerk/nextjs
1150
/**
1251
* Returns a function that accepts a `Request` object and returns whether the request matches the list of
@@ -19,7 +58,7 @@ export type RouteMatcherParam = Array<RegExp | RouteMatcherRoutes> | RegExp | Ro
1958
export const createRouteMatcher = (routes: RouteMatcherParam) => {
2059
const routePatterns = [routes || ''].flat().filter(Boolean);
2160
const matchers = precomputePathRegex(routePatterns);
22-
return (req: Request) => matchers.some(matcher => matcher.test(new URL(req.url).pathname));
61+
return (req: Request) => matchers.some(matcher => matcher.test(normalizePath(new URL(req.url).pathname)));
2362
};
2463

2564
const precomputePathRegex = (patterns: Array<string | RegExp>) => {

0 commit comments

Comments
 (0)