From ecc98573eb8ecc00edff4867e6310c2976d8a9ff Mon Sep 17 00:00:00 2001 From: JustAnDK <89974787+JustAnDK@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:24:11 +0800 Subject: [PATCH] fix: safe cookie parsing (#14292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary This PR fixes a server-side cookie parsing edge case where malformed cookie values throw `URI malformed`, causing socket.io auth to fail and clients to get stuck in infinite workspace loading/syncing. # Observed Behavior - User creates a cloud-backed workspace and invites another user to it. - Second user accepts the invite, awaits approval, and attempts to load the workspace, getting stuck in infinite loading state. - `api/workspaces//docs/` return 404 for those users, as the workspace they are trying to access was not synced to the server. - Server logs show socket.io `CONNECT_ERROR` with `URI malformed`, then connection closed. # Confirmed Trigger An externally-managed `auth_session` cookie containing a raw `%` symbol causes `decodeURIComponent` to throw. This matches the observed socket.io `CONNECT_ERROR`, explaining why some users were affected while the rest were not. # Root Cause The `parseCookies` function calls `decodeURIComponent` on every cookie key/value without guard, so when a malformed percent-encoded value is encountered, `decodeURIComponent` throws, which bubbles into the socket.io auth middleware, aborting the connection. # Fix Wrap `decodeURIComponent` calls in `try/catch`, on failure falling back to the raw key/value. # Testing - Manually regenerating the bad cookie until no malformed parts are present resolves the issue. - With the guard in place, affected users can open shared workspaces with sync successfully completing. ## Summary by CodeRabbit * **Bug Fixes** * Improved cookie parsing robustness so malformed cookie values no longer cause errors; the system now preserves raw cookie values when decoding fails. * **Tests** * Added test coverage to ensure cookie parsing handles invalid/malformed cookie values without throwing. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../src/__tests__/auth/controller.spec.ts | 15 ++++++++++++++ .../backend/server/src/base/utils/request.ts | 20 ++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/backend/server/src/__tests__/auth/controller.spec.ts b/packages/backend/server/src/__tests__/auth/controller.spec.ts index 23f9b7e01..278856184 100644 --- a/packages/backend/server/src/__tests__/auth/controller.spec.ts +++ b/packages/backend/server/src/__tests__/auth/controller.spec.ts @@ -1,10 +1,12 @@ import { randomUUID } from 'node:crypto'; +import { IncomingMessage } from 'node:http'; import { HttpStatus } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import ava, { TestFn } from 'ava'; import Sinon from 'sinon'; +import { parseCookies as safeParseCookies } from '../../base/utils/request'; import { AuthService } from '../../core/auth/service'; import { createTestingApp, @@ -157,6 +159,19 @@ test('should be able to correct user id cookie', async t => { t.is(userIdCookie, u1.id); }); +test('should not throw on parse of a bad cookie', async t => { + const badCookieKey = 'auth_session'; + const badCookieVal = '^13l3PK9qJs*J%X$MOOOIguhkqWvVh7*'; + + const req = { + headers: { cookie: `${badCookieKey}=${badCookieVal}` }, + } as IncomingMessage & { cookies?: Record }; + + t.notThrows(() => safeParseCookies(req)); + + t.is(req.cookies?.[badCookieKey], badCookieVal); +}); + // multiple accounts session tests test('should be able to sign in another account in one session', async t => { const { app } = t.context; diff --git a/packages/backend/server/src/base/utils/request.ts b/packages/backend/server/src/base/utils/request.ts index a4da6a565..eebdb1e94 100644 --- a/packages/backend/server/src/base/utils/request.ts +++ b/packages/backend/server/src/base/utils/request.ts @@ -69,9 +69,23 @@ export function parseCookies( const [key, val] = cookie.split('='); if (key) { - cookies[decodeURIComponent(key.trim())] = val - ? decodeURIComponent(val.trim()) - : val; + const rawKey = key.trim(); + const rawVal = val ? val.trim() : val; + + let safeKey = rawKey; + let safeVal = rawVal; + + try { + safeKey = decodeURIComponent(rawKey); + } catch {} + + if (rawVal) { + try { + safeVal = decodeURIComponent(rawVal); + } catch {} + } + + cookies[safeKey] = safeVal; } return cookies;