Information disclosure through stack traces

Internal information exposed through stack traces in error responses

Description

Including err.stack directly in a response body or HTTP header can disclose internal paths, call flows, and library or framework details. Attackers may use this information to locate vulnerable code and refine further attacks.

Potential impact

  • File paths, source line numbers and module or version details may be disclosed.
  • Call stacks may help an attacker identify vulnerable code or APIs and refine injection attempts.
  • Operating-system usernames and absolute paths may expose details useful for further compromise.

Remediation

  • Return a generic error message to clients and record necessary stack traces only in restricted server logs.
  • Send errors to a central Express error handler to provide consistent messages and status codes.
  • Do not include err.stack in response bodies, JSON or headers.
  • Disable development error pages in production. Use a traceId to correlate an error with internal logs when needed.
  • Mask secrets in logs and limit log access and retention. Moving a stack trace into a log does not protect the log itself.

Examples

Before

javascript
const express = require('express');
const app = express();

app.get('/item/:id', async (req, res) => {
  try {
    // Simulate an error during processing
    throw new Error('DB query failed');
  } catch (err) {
    // BAD: Expose the stack trace in the response body
    return res.status(500).send('Error occurred: ' + err.stack);
  }
});

module.exports = app;

After

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

// Example route
app.get('/item/:id', async (req, res, next) => {
  try {
    // Simulate an error during processing
    throw new Error('DB connection timeout');
  } catch (err) {
    return next(err); // Delegate to the central error handler
  }
});

// Central handler: record the stack only in server logs
app.use((err, req, res, next) => {
  const traceId = crypto.randomUUID();
  console.error(`[${traceId}]`, err.stack); // Internal server log
  res.status(500).json({ message: 'Internal Server Error', traceId }); // Do not expose the stack
});

module.exports = app;

The first example exposes the stack directly in the response body. Node.js rejects header values containing characters such as newlines, so the example focuses on body disclosure.

The second uses a central handler to return only a generic message and traceId, recording the stack in server logs. Its console.error call does not provide production log protection: remove sensitive values and restrict access to logs.

References