When a handler catches an error and sends the raw message or stack trace back in the response, it exposes internal detail that helps an attacker: absolute file paths, framework and library versions, database column names, even fragments of failing queries. The fix is to log the full error where only you can see it and return a generic message with a reference id to the client.
Why it's a problem
Detailed errors turn blind probing into targeted attack. A leaked stack trace reveals the exact stack and versions to match against known exploits, a database error reveals table and column names for injection, and a path reveals your deployment layout. None of it is exploitable on its own, but together it removes the guesswork an attacker would otherwise need.
The pattern
app.get("/api/report", async (req, res) => {
try {
const data = await buildReport(req.query);
res.json(data);
} catch (err) {
// leaks message + stack to the caller
res.status(500).json({ error: err.message, stack: err.stack });
}
});The fix
app.get("/api/report", async (req, res) => {
try {
const data = await buildReport(req.query);
res.json(data);
} catch (err) {
const ref = crypto.randomUUID();
console.error(ref, err); // full detail stays server-side
res.status(500).json({ error: "Something went wrong", ref });
}
});Why AI tools write this
Echoing err.message back to the client is the shortest way to make an error visible during development, and it is genuinely useful while building. The assistant writes the debugging-friendly version and there is no prompt telling it to switch to a production-safe response, so the leak ships as written.
The quick fix
- Log the full error and stack server-side; return a generic message to the client.
- Attach a random reference id to both so you can correlate a report to a log entry.
- Disable framework debug or verbose error pages in production.
- Never include err.stack, query text, or file paths in a response body.