From 29865f836bd90617dedf2dfe793dc68ccde9a6d5 Mon Sep 17 00:00:00 2001 From: RissRIce Date: Thu, 30 Jul 2026 16:55:14 -0600 Subject: [PATCH] fix(web): reject invalid backtest JSON bodies --- apps/web/src/app/api/backtest/route.test.ts | 24 +++++++++++++++++++++ apps/web/src/app/api/backtest/route.ts | 6 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/api/backtest/route.test.ts b/apps/web/src/app/api/backtest/route.test.ts index a0a3710..6106361 100644 --- a/apps/web/src/app/api/backtest/route.test.ts +++ b/apps/web/src/app/api/backtest/route.test.ts @@ -129,6 +129,14 @@ function makeReq(body: unknown) { }); } +function makeRawReq(body: string) { + return new Request('http://test.local/api/backtest', { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: 'Bearer fake-token' }, + body, + }); +} + describe('POST /api/backtest', () => { beforeEach(() => { vi.clearAllMocks(); @@ -166,6 +174,22 @@ describe('POST /api/backtest', () => { expect(body.error).toMatch(/invalid exchange/); }); + it('rejects a null JSON body with 400', async () => { + const { POST } = await importRoute(); + const res = await POST(makeReq(null) as any); + expect(res.status).toBe(400); + expect(await res.json()).toEqual({ error: 'invalid JSON body' }); + expect(fetchHistoricalCandlesMock).not.toHaveBeenCalled(); + }); + + it('rejects malformed JSON with 400', async () => { + const { POST } = await importRoute(); + const res = await POST(makeRawReq('{') as any); + expect(res.status).toBe(400); + expect(await res.json()).toEqual({ error: 'invalid JSON body' }); + expect(getActivePairsMock).not.toHaveBeenCalled(); + }); + it('returns 400 when no pairs are available and none requested', async () => { getActivePairsMock.mockResolvedValueOnce([]); const { POST } = await importRoute(); diff --git a/apps/web/src/app/api/backtest/route.ts b/apps/web/src/app/api/backtest/route.ts index 45bc510..0b551e5 100644 --- a/apps/web/src/app/api/backtest/route.ts +++ b/apps/web/src/app/api/backtest/route.ts @@ -63,7 +63,11 @@ export async function POST(req: NextRequest) { const auth = await authenticate(req); if (!auth) return unauthorized(); - const body = (await req.json().catch(() => ({}))) as BacktestRequest; + const rawBody = await req.json().catch(() => null) as unknown; + if (!rawBody || typeof rawBody !== 'object' || Array.isArray(rawBody)) { + return Response.json({ error: 'invalid JSON body' }, { status: 400 }); + } + const body = rawBody as BacktestRequest; const timeframe = (body.timeframe ?? '5m') as AnalysisTimeframe; if (!TIMEFRAMES.includes(timeframe)) { return Response.json({ error: `invalid timeframe "${timeframe}"`, validTimeframes: TIMEFRAMES }, { status: 400 });