Stack traces and internal error details handed back in API responses are reconnaissance gifts. CWE-209 (Generation of Error Message Containing Sensitive Information) and CWE-200 (Exposure of Sensitive Information to Unauthorized Actors) cover the pattern. OWASP API Security Top 10 2023 API8 (Security Misconfiguration) specifically calls out verbose error responses. A stack trace reveals internal library versions (useful for finding known CVEs), file system paths (useful for traversal attacks), database query structure (useful for injection), and internal service names. Frameworks default to verbose errors in development — that default must be explicitly reversed for production, and the mechanism for that reversal must be tested.
Low because error leakage is information disclosure rather than direct exploitation, but it accelerates every other attack class by revealing internal implementation details.
Centralize all error handling so internal details are logged server-side and a safe, generic message is returned to the client. Never pass error.message or error.stack directly to the response body.
// src/lib/api-error.ts
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
}
}
export function handleApiError(error: unknown): Response {
if (error instanceof ApiError) {
return Response.json({ error: error.message }, { status: error.status })
}
// Log full details server-side
console.error('[API Error]', error)
// Return generic message to client — no stack, no internals
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
// In route handlers:
export async function GET(req: Request) {
try {
// ... logic
} catch (err) {
return handleApiError(err)
}
}
ID: api-security.design-security.error-handling
Severity: low
What to look for: Enumerate every relevant item. Check error handling in API routes. Look at what information is returned in error responses. Verify that stack traces, internal file paths, database query details, and system information are not exposed in responses.
Pass criteria: At least 1 of the following conditions is met. Error responses include a user-friendly message but no technical details. Stack traces and internal information are logged server-side only.
Fail criteria: Error responses include stack traces, SQL queries, internal file paths, or other sensitive system information.
Skip (N/A) when: Never — error handling applies to all APIs.
Detail on fail: "Error responses include full stack traces" or "Failed database queries leak SQL syntax and table names" or "File path errors expose internal directory structure"
Remediation: Implement error handling that hides internal details:
// Bad: Exposes internal details
app.get('/api/users/:id', (req, res) => {
try {
const user = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`)
res.json(user)
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack })
}
})
// Good: Hides internal details
app.get('/api/users/:id', (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
res.json(user)
} catch (error) {
console.error(error) // Log internally
res.status(500).json({ error: 'Internal server error' })
}
})