Skip to content

Detailed error messages

CWE-209CWE-200OWASP A05:2021Updated September 4, 20265 min read

An error message containing a stack trace, a database query or a file path tells an attacker how your application is built. That information saves them the reconnaissance work and enables targeted follow-up attacks. Users need nothing more than a neutral message with a reference number; the details belong in your own logs.

An error message is written to help a developer, and it does that well: it tells you exactly what went wrong, where in the code, with which data. If that same message reaches a visitor, it helps an attacker just as effectively. The sections below cover what such messages contain, what an attacker does with them, and how to handle errors without blinding your own developers.

What are detailed error messages?

We speak of detailed error messages when an application, on encountering an unexpected situation, shows technical information intended for the developer. In practice that means a stack trace with file paths and line numbers, the SQL query that failed, the version of the framework or database server, or the contents of variables at the moment of the error.

The point is not that one such message leads directly to compromise. The point is that an attacker begins their work by reconnoitring: which technology runs here, which versions, how is the code organised, what do the queries look like. A detailed error message supplies all of that at once, without them having to go looking.

Compare it with a building where a diagram of the complete electrical installation hangs on the meter cupboard door. For the engineer that is handy. For anyone with bad intentions it is the floor plan they would otherwise have had to reconstruct.

What does such an error message contain?

Vulnerable:

app.get('/invoice/:id', async (req, res) => {
  try {
    const invoice = await db.query(
      'SELECT * FROM invoices WHERE id = ? AND customer_id = ?',
      [req.params.id, req.user.customerId],
    );
    res.json(invoice);
  } catch (error) {
    res.status(500).send(error.stack);      // everything to the user
  }
});

A request with an unexpected value then produces something like:

Error: ER_PARSE_ERROR: You have an error in your SQL syntax near ''' AND customer_id = 4471'
    at Query.Sequence._packetToError (/opt/portal/node_modules/mysql/lib/...)
    at /opt/portal/src/invoices/repository.js:88:19
    at InvoiceService.find (/opt/portal/src/invoices/service.js:34:7)

Four lines hold a considerable amount of information. The database is MySQL. The application runs in /opt/portal. The code is split into a repository and a service layer. There is a customer_id column, and its value for this user is 4471. And, most importantly: the input evidently lands in the query in a way that lets a single quote break it, a strong indication of SQL injection. What an attacker would otherwise have had to establish through dozens of blind attempts is written out plainly.

Safe:

app.get('/invoice/:id', async (req, res, next) => {
  const invoice = await invoices.find(req.params.id, req.user.customerId)
    .catch(next);
  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

// One central handler for everything that goes wrong
app.use((error, req, res, next) => {
  const reference = randomUUID();

  logger.error({                       // full details, internal only
    reference,
    message: error.message,
    stack: error.stack,
    path: req.originalUrl,
    user: req.user?.id,
  });

  res.status(500).json({
    error: 'Something went wrong. Please contact our service desk.',
    reference,                         // links the message to the log
  });
});

The user receives a usable but uninformative message with a reference number. The developer loses nothing: the full stack trace, path and user sit in the log, linked to that same number. If someone calls the service desk with the reference, the incident is found in one search.

Think also of the error pages your web server or framework generates itself for situations your code never reaches: a non-existent route, an oversized upload, a malformed request. Those fall outside your own handling and often do show version information or a stack trace by default.

What is the impact of verbose error messages?

The severity is usually low to medium, because leaking information grants no access by itself. Its significance lies in the acceleration it gives an attacker.

An attacker who knows your framework, the versions and the structure of your code can search specifically for known vulnerabilities in those exact versions rather than probing broadly. Internal file paths are useful in attacks such as path traversal or when uploading files. And a database error revealing the query shortens exploiting SQL injection from a laborious blind process to a matter of minutes.

There is also a scenario in which the error message is itself the damage. If the exception contains the contents of variables, personal data, tokens or credentials for an external service can sit in there. That happens more often than expected with errors in communication with another API, where the full request including the key used ends up in the message.

How do you detect verbose error messages?

A tester provokes errors where the application processes input: a letter where a number is expected, a quote in a text field, an over-long value, a missing parameter, a malformed JSON request. Then they look at what comes back.

Beyond that, the edges of the application are walked: non-existent paths, wrong HTTP methods, uploads over the limit and requests with unexpected headers. It is precisely there that the web server or framework often responds instead of your own code, with a default page that gives away more. They also check whether the API applies the same discipline as the web interface, and whether a debug mode is enabled anywhere showing extended information. AssistSec additionally looks at the difference between responses: two neutral messages that differ ever so slightly can together still reveal which accounts or records exist.

How do you prevent verbose error messages?

  • Catch errors centrally and give users a neutral message with a unique reference number.
  • Write the full details, stack trace, query, context, only to your own log files.
  • Disable debug and development mode in every environment reachable from the internet.
  • Configure the default error pages of your web server and framework too, not only those of your own code.
  • Show no version information for your framework, server or database in error messages or headers.
  • Make sure APIs return a structured, neutral error object without the underlying exception.
  • Filter sensitive values out of your logs, so tokens and personal data do not end up in them.
  • Keep messages that might differ identical, so they do not reveal existing accounts or records.
  • Test deliberately for error situations as part of your acceptance process, not only the happy path.

Sources

Frequently asked questions

May I show full errors in a test environment?

Yes, and it is practical. The pitfall is that test environments are often reachable from the internet and have the same structure as production. Make sure that environment is shielded, because a stack trace from acceptance reveals virtually the same as one from production.

What should I show the user instead?

A short, neutral message that something went wrong, with a unique reference number. You link that number to the full details in your own logs. The user can pass it to your service desk, and you give nothing away about the cause.

Is an error message without a stack trace safe?

Not automatically. The text itself can reveal what went wrong: a message about a duplicate key confirms a record exists, and a difference between two messages can expose usernames or valid identifiers. Watch what the difference between two responses tells you.

What about error messages from an API?

The same rules apply, and they are broken more often because people assume only their own frontend is looking. Return a structured, neutral error object with a code and a reference, and never the underlying exception or query.

Related articles

Press / to search · Esc