Skip to content
Back to field notes
APIsJune 6, 20266 min read

Verbose API error messages leaking stack traces and SQL in production

An unhandled exception in an AI-generated route can return the raw ORM error, file path, and SQL to any anonymous caller. This note shows how to check your error responses before you share the URL.

APIsError handlingInformation disclosure

A failed query that answers with your stack trace

The concrete risk: a caller sends a malformed request to one of your API routes, the underlying query or parser throws, and the route returns the raw exception. The response body carries a stack trace, the absolute file path on the server, the failing SQL string, and sometimes table and column names. None of that requires a login. Any visitor, crawler, or script that can reach the endpoint can read it.

This is information disclosure (OWASP names it Security Misconfiguration; CWE catalogs it as CWE-209, generation of an error message containing sensitive information). It is not, by itself, a way to read your data. But it is observable from outside, and it hands an attacker the parts they would otherwise have to guess: your stack, your file layout, your ORM, and the shape of your schema.

It helps to separate what is observable from what is only possible. Observable: the verbose body comes back to an unauthenticated request, and you can read it. Possible: that disclosure makes a later SQL injection or path-based attack easier to write. The leak is the confirmed problem. Treat the rest as the reason the leak matters, not as proof anything else is broken.

  • A returned SQL string and column names reveal schema you never published.
  • An absolute file path reveals the server OS, the framework, and the deploy layout.
  • A full stack trace reveals library versions, which map to known weaknesses.

Why AI-built routes propagate raw exceptions

The default for most frameworks in development mode is to show the full error, because that is what a developer wants while building. The job that does not happen automatically is switching that off, and adding a handler that returns a safe, generic error in production.

An AI-generated route optimizes for the path that works. Asked to "add an endpoint that fetches an order by id," the tool writes the happy path: take the id, run the query, return the row. It rarely wraps the query in error handling, because the prompt did not describe what should happen when the id is missing, malformed, or points at nothing. So the unhandled throw bubbles all the way up to the framework's default handler, which in many setups still prints the detail.

  • The route works for valid input, so the missing error branch is invisible from inside the app.
  • Development defaults often carry into the deploy because no one flipped the environment to production. This is the same class of problem as a debug endpoint left open; see debug endpoints open in production.
  • Fast deploy means the public URL exists before anyone sends a deliberately broken request to see what comes back.

Check what your own errors return

You can confirm this on your own app without attacking anything. The method is to send inputs your route did not plan for, then read the response body and status.

Open DevTools and the Network tab, or use a command-line HTTP client against your own API. For each route that takes input, send a deliberately broken request: a missing required field, a string where a number is expected, an id with odd characters, or an oversized payload. Watch the response body and the HTTP status. Do this only against your own deployment.

  • Suspicious result: the body contains a stack trace, an absolute file path (such as one starting with /var/www or a home directory), a SQL fragment with SELECT or WHERE, an ORM class name, or table and column names. A 500 with a long detailed body is the signal.
  • Safer result: a generic message such as "Internal Server Error" or a clean validation message naming only the bad field, with no internals, returned under a correct status (400 for bad input, 404 for not found, 500 with no detail for an unexpected failure).
  • Repeat per route and per input type. One clean route says nothing about the next. Check authenticated routes too while logged out, so you see what an anonymous caller gets when access control fails before the handler runs.

Sending malformed input to your own endpoints is a safe self-check. Sending it to a service you do not own is not, and probing third-party targets is out of scope here. Test only deployments you control.

Return generic errors and log the detail server-side

The fix is to make the detailed error go to your logs, and the generic error go to the caller. Two stages, and the first is where to land safely.

Set the deploy to production mode so the framework stops emitting verbose errors by default, and add a top-level error handler that catches anything unhandled, logs the full detail server-side, and returns a generic body with the right status code. The caller gets "something went wrong" plus maybe a correlation id; you keep the stack trace in your logging system. For expected failures, validate input before the query so you can return a clean 400 or 404 that names the problem without exposing internals.

This fix stops the disclosure. It does not fix the underlying weakness that the leak would have pointed at. If a route is vulnerable to SQL injection, hiding the SQL error does not make the query safe; it only makes the flaw harder to find. Generic errors are a necessary control, not a substitute for parameterized queries, input validation, and authorization checks on each route.

  • Turn off development error detail in the deployed environment first, even before every route has a handler. That alone removes most of the leak.
  • Add a catch-all handler so an unhandled throw returns a generic 500, never the raw exception.
  • Validate and reject bad input early, and return a 404 for not-found rather than letting a query throw.
  • Make sure the disclosure is gone from headers and logs the caller can reach too, not only the JSON body.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check probes your public routes with malformed inputs and inspects the error bodies that come back. It looks for stack trace patterns, SQL keywords, internal file-path strings, and framework error signatures in the response. If a route answers a broken request with its internals, the scan flags the exposure with severity and fix direction. It checks the thing an outside caller can actually trigger and read.

What it does not do is review your route code, confirm whether a leaked query is injectable, or test your authorization logic. Whether the underlying handler is actually exploitable, and whether the right callers are allowed to reach it at all, is work for manual review and code-level testing. A leaked SQL error and an injectable query are different findings; the scan sees the first, not the second.

Next step: pick your most-used input routes, send each a deliberately broken request, and read the body. Then run a focused Launch Check against the public URL before sharing it, so a verbose error shows up in your scan and not in someone else's.

A clean Launch Check on error responses does not prove your routes are safe. It proves the routes it reached did not leak internals on the inputs it tried. It does not exercise every route, every input, or your business logic, and it is not a penetration test or a code review. Treat a flagged leak as a launch blocker, and treat a clean result as one signal among several.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.