From e4a7f47e0f14b7922055b02a56d9901627bef0c8 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 22:46:24 +0000 Subject: [PATCH] fix: validate Cashu token format before placing bets (BUG-S4) Added getDecodedToken validation that rejects malformed tokens with 400 before any DB lookups. Tests cover empty, non-base64, truncated, and random base64 tokens. Co-Authored-By: Claude Opus 4.6 --- server/src/routes/bets.test.ts | 50 ++++++++++++++++++++++++++++++++++ server/src/routes/bets.ts | 8 ++++++ 2 files changed, 58 insertions(+) create mode 100644 server/src/routes/bets.test.ts diff --git a/server/src/routes/bets.test.ts b/server/src/routes/bets.test.ts new file mode 100644 index 0000000..30d1629 --- /dev/null +++ b/server/src/routes/bets.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { Hono } from 'hono' +import { betsRouter } from './bets.js' + +const app = new Hono() +app.route('/api/bets', betsRouter) + +async function placeBet(cashuToken: string) { + return app.request('/api/bets/place', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + fightId: 'test-fight', + pubkey: 'deadbeef', + botId: 'test-bot', + amountSats: 100, + cashuToken, + }), + }) +} + +describe('bets cashu token validation', () => { + it('rejects empty string token', async () => { + const res = await placeBet('') + // Empty string fails the required fields check first + expect(res.status).toBe(400) + }) + + it('rejects non-base64 garbage token', async () => { + const res = await placeBet('not-a-valid-cashu-token!!!') + expect(res.status).toBe(400) + const json = await res.json() as { error: string } + expect(json.error).toBe('Invalid Cashu token format.') + }) + + it('rejects truncated token', async () => { + // A truncated token that starts like cashu but is incomplete + const res = await placeBet('cashuAey') + expect(res.status).toBe(400) + const json = await res.json() as { error: string } + expect(json.error).toBe('Invalid Cashu token format.') + }) + + it('rejects random base64 that is not cashu format', async () => { + const res = await placeBet('eyJhbGciOiJIUzI1NiJ9') + expect(res.status).toBe(400) + const json = await res.json() as { error: string } + expect(json.error).toBe('Invalid Cashu token format.') + }) +}) diff --git a/server/src/routes/bets.ts b/server/src/routes/bets.ts index 9e5712c..89ee717 100644 --- a/server/src/routes/bets.ts +++ b/server/src/routes/bets.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' import { toError } from '../lib/utils.js' +import { getDecodedToken } from '@cashu/cashu-ts' import { db, schema } from '../db/index.js' import { eq, desc } from 'drizzle-orm' import { calculateOdds } from '../engine/odds.js' @@ -59,6 +60,13 @@ betsRouter.post('/place', rateLimit(60_000, 10), async (c) => { return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400) } + // Validate Cashu token format before any DB lookups + try { + getDecodedToken(cashuToken) + } catch { + return c.json({ error: 'Invalid Cashu token format.' }, 400) + } + // Verify fight is still open const fight = await db.select().from(schema.fights) .where(eq(schema.fights.id, fightId)).limit(1)