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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 09:30:55 +00:00
co-authored by Claude Opus 4.6
parent 21bb46c1b0
commit 2fea2bf8c9
+45
View File
@@ -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.<anonymous> (/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/)
}
})
})