Preliminary Checks
Reproduction
https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030
Publishable key
Not applicable. The throw happens inside the ClerkRequest constructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.
Description
ClerkRequest.decodeCookieValue runs decodeURIComponent over the raw Cookie header with no try/catch, in packages/backend/src/tokens/clerkRequest.ts#L96-L103:
private parseCookies(req: Request) {
const cookiesRecord = parse(this.decodeCookieValue(req.headers.get('cookie') || ''));
return new Map(Object.entries(cookiesRecord));
}
private decodeCookieValue(str: string) {
return str ? str.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent) : str;
}
decodeURIComponent throws URIError: URI malformed on any percent-escape that isn't valid UTF-8. Because parseCookies is called from the constructor (L53), the error escapes createClerkRequest, which is the first thing authenticateRequest does — so the request fails before any auth logic runs.
Two properties widen the blast radius well past the one cookie that is malformed:
- The decode is applied to the whole header, not per value. Any cookie on the domain can trigger it, including ones Clerk never set and never reads — analytics, ad tooling, third-party scripts.
- The bad value persists in the browser until it expires. Every subsequent request from that client fails, not just one.
Steps to reproduce:
npm install @clerk/backend@3.15.1
- Call
createClerkRequest with a request whose Cookie header contains a malformed escape:
import { createClerkRequest } from '@clerk/backend/internal';
createClerkRequest(new Request('https://example.com/', {
headers: { cookie: '__session=abc; analytics_id=%E2%9' },
}));
- Observe
URIError: URI malformed.
Expected behavior:
A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.
Actual behavior:
URIError: URI malformed
at decodeURIComponent (<anonymous>)
at String.replace (<anonymous>)
at ClerkRequest.decodeCookieValue (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6491:22)
at ClerkRequest.parseCookies (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6487:57)
at new ClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6450:25)
at createClerkRequest (.../@clerk/backend/dist/chunk-DDFIPK3V.mjs:6496:37)
Inputs that throw, all verified against 3.15.1:
Cookie header |
Why |
x=%E2%9 |
escape truncated mid-sequence; %E2 decodes alone as an incomplete UTF-8 lead byte |
x=%98 |
lone continuation byte |
x=%C0%80 |
overlong UTF-8 encoding |
The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.
How it surfaces in a Next.js app. @clerk/nextjs 7.x on Vercel (Node.js 22 runtime), clerkMiddleware() in proxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:
Error running the exported Web Handler: URIError: URI malformed
at decodeURIComponent (<anonymous>)
at String.replace (<anonymous>)
at aJ.decodeCookieValue (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40399)
at aJ.parseCookies (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40317)
at new aJ (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:39648)
at aq (.next/server/chunks/[root-of-the-server]__0tiorp-._.js:49:40531)
Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.
Present in the latest release. The code above is current on main, and the published build is the same: @clerk/backend@3.15.1/dist/internal.js lines 7007-7013.
Possibly related, same two lines. The regex (%[0-9A-Z]{2})+ matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:
decodeCookieValue('x=%c3%a9') // 'x=%c3%a9' — left raw
decodeCookieValue('x=%C3%A9') // 'x=é'
Percent-encoding is case-insensitive, so both should decode to the same value.
Prior public report. ViewComfy/ViewComfy#147 has the identical ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequest stack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo for URI malformed, URIError, decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.
Environment
System:
OS: Linux 6.12 Debian GNU/Linux 13 (trixie)
CPU: (5) arm64
Binaries:
Node: 24.18.0
npm: 11.16.0
pnpm: 11.11.0
npmPackages:
@clerk/backend: 3.15.1 => 3.15.1
Also reproduced in production on:
Vercel, Node.js 22.x runtime
next: 16.2.6
@clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)
Preliminary Checks
Reproduction
https://gist.github.com/davidbarratt/08d1334f2c9dde6fa5cde0da7c3a4030
Publishable key
Not applicable. The throw happens inside the
ClerkRequestconstructor, in a string function, before any Clerk API call — the reproduction runs with no instance, no key, and no network access.Description
ClerkRequest.decodeCookieValuerunsdecodeURIComponentover the rawCookieheader with notry/catch, inpackages/backend/src/tokens/clerkRequest.ts#L96-L103:decodeURIComponentthrowsURIError: URI malformedon any percent-escape that isn't valid UTF-8. BecauseparseCookiesis called from the constructor (L53), the error escapescreateClerkRequest, which is the first thingauthenticateRequestdoes — so the request fails before any auth logic runs.Two properties widen the blast radius well past the one cookie that is malformed:
Steps to reproduce:
npm install @clerk/backend@3.15.1createClerkRequestwith a request whoseCookieheader contains a malformed escape:URIError: URI malformed.Expected behavior:
A cookie value that cannot be percent-decoded is left as its raw value or skipped, and the rest of the header parses normally. A cookie unrelated to Clerk should not be able to fail the request.
Actual behavior:
Inputs that throw, all verified against 3.15.1:
Cookieheaderx=%E2%9%E2decodes alone as an incomplete UTF-8 lead bytex=%98x=%C0%80The truncated case is the one we hit in production. A client-side script writing a value that gets clipped at the browser's ~4096-byte per-cookie limit lands mid-escape, and that alone is enough to make every later request 500.
How it surfaces in a Next.js app.
@clerk/nextjs7.x on Vercel (Node.js 22 runtime),clerkMiddleware()inproxy.ts. The proxy returns HTTP 500 and the request never reaches our handler. The minified frames map onto the same four methods:Because it happens in middleware, there is no application-level place to catch it — the exception is raised while Clerk is constructing its own request wrapper.
Present in the latest release. The code above is current on
main, and the published build is the same:@clerk/backend@3.15.1/dist/internal.jslines 7007-7013.Possibly related, same two lines. The regex
(%[0-9A-Z]{2})+matches only uppercase hex digits, so lowercase escapes are skipped while uppercase ones in the same value are decoded:Percent-encoding is case-insensitive, so both should decode to the same value.
Prior public report. ViewComfy/ViewComfy#147 has the identical
ClerkRequest.decodeCookieValue → parseCookies → new ClerkRequeststack, reported in July 2025 and closed with no fix and no upstream link. I searched this repo forURI malformed,URIError,decodeCookieValue, and malformed-cookie phrasings and did not find an existing issue.Environment
System: OS: Linux 6.12 Debian GNU/Linux 13 (trixie) CPU: (5) arm64 Binaries: Node: 24.18.0 npm: 11.16.0 pnpm: 11.11.0 npmPackages: @clerk/backend: 3.15.1 => 3.15.1 Also reproduced in production on: Vercel, Node.js 22.x runtime next: 16.2.6 @clerk/nextjs: 7.2.1 (@clerk/backend 3.2.11)