From c0bd6d732dc0093fd7a58d05c0528d7282c07a3a Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 14:22:48 -0400 Subject: [PATCH] fix(04-07): add owner-only guard for isShared on PATCH /api/lists/:id (T-04-08) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Immediately after access check, return 403 if patch.isShared !== undefined and !access.isOwner — blocks sharees from mutating list_shares - Guard message: 'Only the list owner can change sharing settings' - Sharees may still PATCH { name } (rename test stays green) - Update stale comment: 'Reconcile list_shares on visibility change (owner only)' - Closes T-04-08 (elevation of privilege) and T-04-05 (shared root cause) --- apps/api/src/routes/lists.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/lists.ts b/apps/api/src/routes/lists.ts index 58497aa..50efe75 100644 --- a/apps/api/src/routes/lists.ts +++ b/apps/api/src/routes/lists.ts @@ -331,6 +331,12 @@ listsRouter.patch('/:id', zValidator('json', patchListSchema), async (c) => { return c.json({ error: 'Access denied' }, 403) } + // T-04-08 / T-04-05: owner-only guard for isShared mutations. + // A sharee may rename a list (patch.name) but must never mutate list_shares. + if (patch.isShared !== undefined && !access.isOwner) { + return c.json({ error: 'Only the list owner can change sharing settings' }, 403) + } + const prevIsShared = access.listRow.isShared const newIsShared = patch.isShared ?? prevIsShared @@ -341,7 +347,7 @@ listsRouter.patch('/:id', zValidator('json', patchListSchema), async (c) => { await db.update(lists).set(updateValues).where(eq(lists.id, listId)) - // Reconcile list_shares on visibility change (owner only affects shares) + // Owner-only: reconcile list_shares on visibility change if (patch.isShared !== undefined && patch.isShared !== prevIsShared) { if (newIsShared) { // false → true: insert shares for all other members (except owner)