From 3323e4e2dbc0828ec0842da64530f0e0d8103337 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 1 Sep 2026 13:06:56 +0000 Subject: [PATCH 1/2] fix(security): add params schema validation to ingest id routes Add TypeBox params schema (ingestId as a non-empty numeric string) to the GET, PATCH and DELETE /ingest/:ingestId routes so ingestId is validated by Fastify before parseInt, rejecting malformed input with 400. Closes #257 Co-Authored-By: Claude Opus 4.7 --- src/api_ingests.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/api_ingests.ts b/src/api_ingests.ts index 1f50159..8a263e2 100644 --- a/src/api_ingests.ts +++ b/src/api_ingests.ts @@ -135,6 +135,9 @@ const apiIngests: FastifyPluginCallback = ( { schema: { description: 'Retrieves an ingest.', + params: Type.Object({ + ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' }) + }), response: { 200: Ingest, 500: Type.String() @@ -175,6 +178,9 @@ const apiIngests: FastifyPluginCallback = ( schema: { description: 'Modify an existing Ingest. By changing the label, the deviceOutput or the deviceInput, the ingest is updated and the new ingest is returned.', + params: Type.Object({ + ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' }) + }), body: PatchIngest, response: { 200: PatchIngestResponse, @@ -247,6 +253,9 @@ const apiIngests: FastifyPluginCallback = ( { schema: { description: 'Deletes a Ingest.', + params: Type.Object({ + ingestId: Type.String({ minLength: 1, pattern: '^[0-9]+$' }) + }), response: { 200: Type.String(), 500: Type.String() From 9ca328e937e06e30b733c484fdca9e26b8a4f231 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 16 Sep 2026 10:39:56 +0000 Subject: [PATCH 2/2] test(security): add ingestId param validation regression tests for #257 Covers the exact failure path from the code review: GET/PATCH/DELETE /api/v1/ingest/:ingestId reject non-numeric, empty, float, negative and special-character ingestId with 400 (Fastify schema validation runs before the 501 preHandler), while a valid numeric id passes validation (501, not 400). Co-Authored-By: Claude Sonnet 4.6 --- src/api_validation.test.ts | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/api_validation.test.ts b/src/api_validation.test.ts index d0120de..95fb43f 100644 --- a/src/api_validation.test.ts +++ b/src/api_validation.test.ts @@ -447,4 +447,53 @@ describe('Input Validation', () => { expect(response.statusCode).toBe(410); }); }); + + // ── Ingest :ingestId param validation (regression for #257) ──── + // Routes are 501-gated by a preHandler, but Fastify runs schema + // validation before preHandler, so a bad ingestId is rejected with + // 400 while a valid numeric id falls through to the 501 stub. + + describe('Ingest :ingestId param validation', () => { + test.each([ + ['non-numeric', 'abc'], + ['empty-ish', ' '], + ['float', '1.5'], + ['negative', '-1'], + ['special characters', 'id!@#'] + ])( + 'GET /ingest/:ingestId rejects %s ingestId with 400', + async (_label, badId) => { + const response = await server.inject({ + method: 'GET', + url: `/api/v1/ingest/${encodeURIComponent(badId)}` + }); + expect(response.statusCode).toBe(400); + } + ); + + test('PATCH /ingest/:ingestId rejects non-numeric ingestId with 400', async () => { + const response = await server.inject({ + method: 'PATCH', + url: '/api/v1/ingest/abc', + body: { label: 'valid-label' } + }); + expect(response.statusCode).toBe(400); + }); + + test('DELETE /ingest/:ingestId rejects non-numeric ingestId with 400', async () => { + const response = await server.inject({ + method: 'DELETE', + url: '/api/v1/ingest/abc' + }); + expect(response.statusCode).toBe(400); + }); + + test('GET /ingest/:ingestId with a valid numeric id passes validation (501, not 400)', async () => { + const response = await server.inject({ + method: 'GET', + url: '/api/v1/ingest/123' + }); + expect(response.statusCode).toBe(501); + }); + }); });