← Toutes les actualités
data-exposure5 min de lecture

Your Error Messages Are Leaking Secrets (Yes, Even the Ones AI Wrote)

Par Seaworthy · 23 octobre 2026

A red danger sign with bold warning text against a dark abstract background

Photo by Jakub Zerdzicki on Unsplash

I keep seeing the same thing in code reviews: a perfectly functional API that returns a stack trace to the client when something breaks. It is not a new problem, but it has a new engine. Developers using AI coding assistants are shipping error handling that looks reasonable until you read the response body. The assistant writes a try/catch, logs the exception, and returns str(e) or the full traceback. It compiles. Tests pass. Nobody looks at the failure path because the happy path is what gets demoed.

That is a data-exposure bug wearing a helpful mask. Error messages are supposed to help developers debug. Returning them to clients hands attackers a map of your internals: file paths, library versions, SQL fragments, environment variable names, sometimes connection strings. I have seen a stack trace that included a full database URL with credentials in the query string. The developer was surprised. The AI assistant had written that line without comment, and the review focused on the business logic.

The pattern shows up in every language. In Python it is return jsonify({"error": str(e)}) or a framework debug page left on in production. In Node it is res.status(500).send(err.stack). In Java it is a default error controller that serializes the exception. The common thread is convenience: the full detail is right there, and exposing it feels faster than mapping it.

Here is the shape of the problem in a small Express handler:

app.get('/orders/:id', async (req, res) => {
  try {
    const order = await db.getOrder(req.params.id);
    res.json(order);
  } catch (err) {
    res.status(500).json({
      error: err.message,
      stack: err.stack,
      query: err.sql,
    });
  }
});

An attacker probing for SQL injection gets immediate feedback: syntax errors reveal the query structure, and stack frames reveal the driver and version. That feedback loop shortens their work. You gave them a debugger.

The fix is not complicated, which makes the frequency more frustrating. Log the detail on the server. Return a generic message and a correlation ID to the client. Then use that ID to find the full record in your logs.

app.get('/orders/:id', async (req, res) => {
  try {
    const order = await db.getOrder(req.params.id);
    res.json(order);
  } catch (err) {
    const incidentId = crypto.randomUUID();
    logger.error({ incidentId, err, route: req.path }, 'order lookup failed');
    res.status(500).json({ error: 'Internal error', incidentId });
  }
});

That is the whole fix. The client gets enough to report the problem. You get everything you need to debug it. The attacker gets nothing.

Why does this keep happening with AI-assisted code? Assistants are trained on a lot of tutorials and sample code, and sample code optimizes for showing the error, not for hiding it. When you ask for a handler, you get the handler that prints the exception because that is what makes the example instructive. The model has no stake in your production posture. It will happily write debug=True into a config if the surrounding code suggests a development setup.

There is a second layer to the problem that rarely gets discussed: error responses are often assembled by parts of the stack nobody owns. A reverse proxy adds its own headers. A framework middleware wraps your response. Your application catches what falls through. When an assistant generates a handler that returns raw exception text, it does so inside a chain of components that may each assume someone else is sanitizing the output. Nobody is. The response that reaches the client is the union of every layer's default behavior, and defaults tend to be generous with detail.

This is why a single middleware owning the response shape matters more than fixing one handler. If the final step before bytes leave your service normalizes every error path, then a stray err.stack from a generated handler never becomes a leak. It becomes internal data that the middleware discards. The middleware should map internal exceptions to a small set of public error codes, attach the correlation ID, and refuse to pass through any field it does not recognize.

My reaction is blunt: stop trusting generated error paths until you have read them. Treat every catch block as a security boundary. If the response body can contain a string from an exception, ask what could be inside that string. File paths, hostnames, tokens, and query text are all fair game. The same applies to validation errors: a helpful message like column 'user_email' does not exist tells an attacker your schema.

A few habits cut the risk fast. Turn off framework debug modes in production and verify it with a request, not a config file. Add a test that asserts error responses do not contain stack frames or known internal strings. Centralize error handling so one middleware owns the response shape. Review generated code with the failure path in mind, and prompt your assistant to log details server-side and return opaque messages.

The uncomfortable part is that verbose errors feel like good engineering. They are good for development. In production they are an information leak with a friendly face. If you would not print your config to stdout on every request, do not print your exceptions to the client.

Seaworthy is a developer security company that reviews code and cloud configuration for exposure issues like this one.

Cet article a été généré par IA et résume des sources publiquement disponibles.

Par les créateurs de Seaworthy

Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.

Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.

$ npx seaworthycode