From 9c7267e8c7bdb0bcbbaf37711ecc76dc0c23e1fd Mon Sep 17 00:00:00 2001 From: brkalow Date: Tue, 11 Aug 2026 14:15:09 -0500 Subject: [PATCH] fix(backend): decode cookie values after parsing --- .changeset/tidy-cookies-smile.md | 5 +++ .../src/tokens/__tests__/clerkRequest.test.ts | 32 ++++++++++++++++--- packages/backend/src/tokens/clerkRequest.ts | 6 +--- 3 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 .changeset/tidy-cookies-smile.md diff --git a/.changeset/tidy-cookies-smile.md b/.changeset/tidy-cookies-smile.md new file mode 100644 index 00000000000..4088ccc78ea --- /dev/null +++ b/.changeset/tidy-cookies-smile.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Decode request cookie values only after parsing cookie boundaries, preserving malformed percent encodings and preventing encoded delimiters from being treated as separate cookies. diff --git a/packages/backend/src/tokens/__tests__/clerkRequest.test.ts b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts index 1b75b88cd44..1440d9a1a30 100644 --- a/packages/backend/src/tokens/__tests__/clerkRequest.test.ts +++ b/packages/backend/src/tokens/__tests__/clerkRequest.test.ts @@ -89,12 +89,36 @@ describe('createClerkRequest', () => { expect(req.cookies.get('foo')).toBe('bar'); }); - it('should parse and return cookies with special characters', () => { + it('should decode values after parsing cookie pairs', () => { const req = createClerkRequest( - new Request('http://localhost:3000', { headers: new Headers({ cookie: 'foo=%20bar%3B%20baz%3Dqux' }) }), + new Request('http://localhost:3000', { + headers: new Headers({ cookie: 'foo=%20bar%3B%20baz%3Dqux; after=parsed' }), + }), ); - expect(req.cookies.get('foo')).toBe('bar'); - expect(req.cookies.get('baz')).toBe('qux'); + + expect(req.cookies.get('foo')).toBe(' bar; baz=qux'); + expect(req.cookies.get('baz')).toBeUndefined(); + expect(req.cookies.get('after')).toBe('parsed'); + }); + + it.each(['%E2%9', '%98', '%C0%80'])('should preserve a malformed encoded cookie value: %s', value => { + const req = createClerkRequest( + new Request('http://localhost:3000', { + headers: new Headers({ cookie: `__session=abc; analytics_id=${value}; after=parsed` }), + }), + ); + + expect(req.cookies.get('__session')).toBe('abc'); + expect(req.cookies.get('analytics_id')).toBe(value); + expect(req.cookies.get('after')).toBe('parsed'); + }); + + it('should decode lowercase percent escapes', () => { + const req = createClerkRequest( + new Request('http://localhost:3000', { headers: new Headers({ cookie: 'foo=%c3%a9' }) }), + ); + + expect(req.cookies.get('foo')).toBe('é'); }); it('should parse and return cookies even if no cookie header exists', () => { diff --git a/packages/backend/src/tokens/clerkRequest.ts b/packages/backend/src/tokens/clerkRequest.ts index 8b02266c643..7559661a438 100644 --- a/packages/backend/src/tokens/clerkRequest.ts +++ b/packages/backend/src/tokens/clerkRequest.ts @@ -94,13 +94,9 @@ class ClerkRequest extends Request { } private parseCookies(req: Request) { - const cookiesRecord = parse(this.decodeCookieValue(req.headers.get('cookie') || '')); + const cookiesRecord = parse(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; - } } export const createClerkRequest = (...args: ConstructorParameters): ClerkRequest => {