diff --git a/server/src/routes/error-handling.test.ts b/server/src/routes/error-handling.test.ts index 8493449..d31100f 100644 --- a/server/src/routes/error-handling.test.ts +++ b/server/src/routes/error-handling.test.ts @@ -49,4 +49,49 @@ describe('global error handler', () => { const res = await app.request('/test') expect(res.status).toBe(500) }) + + it('production error responses never contain stack traces or file paths', async () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + const dangerousMessages = [ + 'ENOENT: no such file or directory, open /Users/dorian/Projects/botfights/server/data/db.sqlite', + 'Error at Object. (/app/server/src/engine/orchestrator.ts:42:15)', + 'ReferenceError: x is not defined\n at /app/server/src/routes/fights.ts:100:5', + ] + + for (const message of dangerousMessages) { + const app = new Hono() + app.onError((_err, c) => { + const msg = process.env.NODE_ENV === 'production' ? 'Internal server error' : _err.message + return c.json({ error: msg }, 500) + }) + app.get('/test', () => { throw new Error(message) }) + const res = await app.request('/test') + const body = await res.json() as { error: string } + expect(body.error).toBe('Internal server error') + expect(body.error).not.toContain('.ts') + expect(body.error).not.toContain('.js') + expect(body.error).not.toContain('/src/') + expect(body.error).not.toContain('at ') + expect(body.error).not.toContain('ENOENT') + } + + process.env.NODE_ENV = originalEnv + }) + + it('no route file contains err.stack in JSON response', async () => { + // Static analysis: verify no route file passes stack traces to c.json() + const fs = await import('fs') + const path = await import('path') + const routesDir = path.resolve(import.meta.dirname || '.', '.') + const files = fs.readdirSync(routesDir).filter(f => f.endsWith('.ts') && !f.includes('.test.')) + + for (const file of files) { + const content = fs.readFileSync(path.join(routesDir, file), 'utf-8') + // err.stack should never appear in c.json() calls + expect(content).not.toMatch(/c\.json\([^)]*err\.stack/) + expect(content).not.toMatch(/c\.json\([^)]*error\.stack/) + } + }) })