From 32bdd1e92d5c3aa310c5ae8931b860a06f5440d6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 20:32:26 -0400 Subject: [PATCH] fix(19): WR-07 reject route ids with trailing garbage via strict integer parse --- apps/api/src/routes/admin.ts | 21 +++++++++++++++++---- apps/api/tests/routes/admin.test.ts | 13 +++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 507f3e4..aa2c183 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -78,6 +78,19 @@ const noEchoHook = (result: { success: boolean }, c: Context) => { } }; +/** + * WR-07: strictly parse a positive-integer route param. parseInt('12abc', 10) returns 12 + * and passes an isNaN guard, silently accepting malformed ids. Number('12abc') is NaN, so + * Number.isInteger(Number(raw)) rejects trailing garbage. Returns null for anything that is + * not a whole positive integer (empty, '12abc', '1.5', '-3', '0', etc.) so the caller can 400. + */ +function parsePositiveIntParam(raw: string | undefined): number | null { + if (raw === undefined || raw.trim() === '') return null; + const n = Number(raw); + if (!Number.isInteger(n) || n <= 0) return null; + return n; +} + // --------------------------------------------------------------------------- // GET /api/admin/members // @@ -217,8 +230,8 @@ adminRouter.post( '/members/:id/password', zValidator('json', resetPasswordSchema, noEchoHook), async (c) => { - const targetId = parseInt(c.req.param('id'), 10); - if (isNaN(targetId)) { + const targetId = parsePositiveIntParam(c.req.param('id')); + if (targetId === null) { return c.json({ error: 'Invalid member id' }, 400); } @@ -316,8 +329,8 @@ adminRouter.get('/calendars', async (c) => { // --------------------------------------------------------------------------- adminRouter.put('/calendars/:id/shared', async (c) => { - const targetId = parseInt(c.req.param('id'), 10); - if (isNaN(targetId)) { + const targetId = parsePositiveIntParam(c.req.param('id')); + if (targetId === null) { return c.json({ error: 'Invalid calendar id' }, 400); } diff --git a/apps/api/tests/routes/admin.test.ts b/apps/api/tests/routes/admin.test.ts index 870b5e8..bfc735e 100644 --- a/apps/api/tests/routes/admin.test.ts +++ b/apps/api/tests/routes/admin.test.ts @@ -483,6 +483,19 @@ describe('PUT /api/admin/calendars/:id/shared', () => { .limit(1); expect(rowA.isShared).toBe(true); }); + + it('WR-07: rejects a calendar id with trailing garbage (e.g. "1abc") with 400', async () => { + const adminId = await seedUser('admin-shared-badid', true); + currentDevUserId = adminId; + const app = await getApp(); + + // parseInt('1abc', 10) === 1 would have silently accepted this; the strict + // Number.isInteger parse must reject it as a malformed id. + const res = await app.fetch(jsonRequest('PUT', '/api/admin/calendars/1abc/shared')); + expect(res.status).toBe(400); + const body = (await res.json()) as { error: string }; + expect(body.error).toBe('Invalid calendar id'); + }); }); // ===========================================================================