From 2fea2bf8c9511fbb5dd98131522627b3ba323b4b Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 09:30:55 +0000 Subject: [PATCH] test: verify error responses never leak stack traces or file paths Add tests confirming production error handler sanitizes all internal errors (ENOENT, stack traces, file paths). Add static analysis test verifying no route file passes err.stack to c.json() responses. Co-Authored-By: Claude Opus 4.6 --- server/src/routes/error-handling.test.ts | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) 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/) + } + }) })