From d88b3348a2bb301a5a5ae41d968ab92d378359fa Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Fri, 15 May 2026 15:00:44 +0000 Subject: [PATCH] batch3 T2 review fix: move PATCH count+bounds check inside sql.begin A concurrent DELETE between the count read and the transaction could allow an invalid position value to slip in. Mirror the POST fix by validating count + bounds inside the transaction. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/server/src/routes/panes.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/server/src/routes/panes.ts b/apps/server/src/routes/panes.ts index 43cc30e..df48b48 100644 --- a/apps/server/src/routes/panes.ts +++ b/apps/server/src/routes/panes.ts @@ -147,20 +147,18 @@ export function registerPaneRoutes(app: FastifyInstance, sql: Sql): void { const sid = pane.session_id; const oldPos = pane.position; - // Validate position if provided - if (position !== undefined) { - const countRows = await sql<{ n: number }[]>` - SELECT COUNT(*)::int AS n FROM session_panes WHERE session_id = ${sid} - `; - const count = countRows[0]?.n ?? 0; - if (position < 0 || position >= count) { - reply.code(400); - return { error: `position must be between 0 and ${count - 1}` }; - } - } - // Apply position and/or state changes atomically + let patchError: string | null = null; await sql.begin(async (tx) => { + if (position !== undefined) { + const countRows = await tx<{ n: number }[]>` + SELECT COUNT(*)::int AS n FROM session_panes WHERE session_id = ${sid} + `; + const count = countRows[0]?.n ?? 0; + if (position < 0 || position >= count) { + throw `position must be between 0 and ${count - 1}`; + } + } if (position !== undefined && position !== oldPos) { await movePane(tx, req.params.id, sid, oldPos, position); } @@ -170,8 +168,19 @@ export function registerPaneRoutes(app: FastifyInstance, sql: Sql): void { WHERE id = ${req.params.id} `; } + }).catch((err: unknown) => { + if (typeof err === 'string') { + patchError = err; + } else { + throw err; + } }); + if (patchError !== null) { + reply.code(400); + return { error: patchError }; + } + const [updated] = await sql` SELECT id, session_id, position, kind, state, created_at FROM session_panes WHERE id = ${req.params.id}