Learn how CWE-209 covers the risks of error messages that leak database queries, stack traces, and system details to users who should never see them.

Applications encounter errors constantly: invalid user input, failed database queries, missing files, and unexpected conditions. How an application handles these errors matters enormously. When something goes wrong, there are two audiences for the error information: the developer, who needs full details to diagnose and fix the problem, and the user, who needs to know that something went wrong and what to do next. CWE-209 occurs when sensitive information meant only for developers is included in error messages that reach users.

CWE-209 Overview

The Common Weakness Enumeration (CWE) is a list of software and hardware security weaknesses. The CWEs are numbered, and each one describes a specific type of mistake that could lead to a security vulnerability. Understanding these common weaknesses helps identify potential security problems, both for the developer and an attacker.

CWE-209 occurs when error messages shown to users contain information that helps an attacker understand the internal workings of a system. This includes raw SQL queries, stack traces, file paths, server software versions, database engine names, configuration details, or any other internal information that was not meant to be public. The weakness is dangerous because attackers deliberately trigger errors to extract this information. A single revealing error message can confirm a vulnerability, identify the technology stack, and provide enough detail to craft a working exploit.

Example 1: Raw SQL Query in Error Response

A login form passes user input directly into a database query, and displays the error when it fails:

async function login(username: string, password: string) {
  const query = (
    `SELECT * FROM users WHERE username='${username}'`
    + ` AND password='${password}'`
  )

  try {
    const result = await db.execute(query)

    return result
  } catch (error) {
    return `Login failed. Query: ${query}. Error: ${error}`
  }
}

This is a weakness because entering ' as the username causes the query to fail, and the error response reveals the exact SQL query to the user. An attacker now knows the table name, the column names, the field order, and the fact that input is not sanitized. This information makes it much easier to craft an SQL injection (SQLi) attack. The fix is to return a generic message to the user ("Login failed. Please try again.") and log the detailed error only on the server-side.

Example 2: Stack Trace Exposed to the User

A web application displays the full stack trace when an unhandled exception occurs:

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception exception) {
    StringWriter stringWriter = new StringWriter();
    exception.printStackTrace(new PrintWriter(stringWriter));

    return ResponseEntity.status(500).body(stringWriter.toString());
}

This is a weakness because the stack trace reveals the application's internal structure: class names, method names, file paths, line numbers, and library versions. An attacker can use this information to identify known vulnerabilities in specific library versions, understand the application's architecture, and target specific code paths. The fix is to log the stack trace internally and return only a generic error response to the user.

Example 3: Database Engine Revealed in Error

An application propagates the raw database error message to the user:

function getUser($id) {
    $result = mysqli_query($conn, "SELECT * FROM users WHERE id=$id");
    if (!$result) {
        die("Database error: " . mysqli_error($conn));
    }

    return mysqli_fetch_assoc($result);
}

This is a weakness because mysqli_error() returns messages like You have an error in your SQL syntax... check the manual that corresponds to your MySQL 8.0 server. This tells the attacker the database engine and version, confirms the SQL injection vulnerability, and provides syntax hints for crafting a working payload. Different databases have different SQL syntax, comment characters, and metadata tables, so knowing the engine is a significant advantage. The fix is to check $result and return a generic error while logging the mysqli_error() output server-side.

Example 4: File Path and Server Details in Error Page

A web server is configured to show detailed error pages in production:

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

// Sends full error details to the client in production.
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack,
    path: req.path
  })
})

This is a weakness because the error handler sends the full exception details to the client as JSON. A stack trace from a Node.js application reveals the server's directory structure through absolute file paths, the versions of every library in node_modules, and the internal call chain that led to the error. An attacker who triggers a 500 error can read this directly in the browser's developer tools or with any HTTP client. The fix is to log the full error server-side and return only a generic response to the client:

app.use((err, req, res, next) => {
  const errorId = crypto.randomUUID()
  console.error(errorId, err)
  res.status(500).json({ 
    error: 'Something went wrong. Please contact support with ' +
      'reference: ' + errorId
  })
})

What to Remember

The most important points to keep in mind about error messages containing sensitive information are:

  • Error messages shown to users should be generic and give no information about the internal workings of the system
  • Full error details including SQL queries, stack traces, file paths, and version information should be logged server-side, never sent to the user
  • Attackers deliberately trigger errors to extract information, so any detail in an error response should be treated as potentially useful to an attacker
  • The database engine, server software, framework version, and library versions revealed in error messages can all be used to look up known vulnerabilities
  • A raw SQL query in an error message can single-handedly enable a SQL injection attack by revealing the query structure and confirming that input is unsanitized
  • Development and production environments should have different error handling configurations. Detailed errors may be acceptable locally but must never reach production users
  • Generic error messages should include a reference number or timestamp so users can report issues and developers can correlate them with server logs

Want to learn more about security weaknesses? I'm working through the CWE list and doing writeups for security challenges. Follow along for more articles like this one.