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

Debug endpoints left open in production AI-built apps

AI coding tools scaffold convenience routes like /debug and /api/status that often ship unauthenticated. This note shows how to find them on your own app and close the ones that leak internal data before launch.

ExposureDebug routesInformation disclosure

A debug route that returns config is open to anyone who guesses the path

The concrete risk is direct: a route like /debug, /api/test, /api/status, /api/__health, /api/env, or /_debug that returns environment values, config, stack traces, dependency versions, or internal JSON, with no login in front of it. If the route is reachable on your public URL and it answers from a logged-out session, then anyone who requests that path gets whatever it returns. No exploit, no credentials, just a GET request to a path that was meant for you during development.

What is observable from outside is narrow and exact: which of these paths respond, and what the response body contains. What is only possible until you look is whether the response leaks something sensitive. A route that returns a bare 200 OK is fine. A route that returns DATABASE_URL, an API key, a full stack trace with file paths, or a JSON dump of internal records is a launch blocker. The path name alone does not tell you which one you have, so the check below is about reading the response, not just finding the route.

  • A debug or env route that echoes environment variables exposes secrets directly.
  • A status or health route can disclose internal hostnames, framework and dependency versions, and feature-flag names that map your stack for an attacker.
  • A route that returns a stack trace on error reveals file paths, library versions, and sometimes query fragments.
  • This is information disclosure (CWE-200 territory), and often a missing authentication check on a sensitive function, not a broken login.

Why AI-built apps ship these routes

AI coding tools scaffold convenience routes while building, because they make the app easier to develop and demo. A prompt that says "add a health check" or "help me debug the deploy" produces a working endpoint fast. The route does its job in development, the app looks finished, and the route stays in the production build because nothing in the flow forces a decision about whether it should be public.

The gating step is the one that does not happen automatically. Adding a route is a generation task the tool does well. Removing it from production, or putting an auth check in front of it, is a review task that depends on someone remembering the route exists. Fast deploy makes this worse: the preview becomes the launch URL before anyone audits the route list, so the debug scaffold ships alongside the real product.

  • The route works, so its presence is invisible from inside the running app.
  • Health and status endpoints feel harmless, so they are rarely the thing a launch review looks at, even when they disclose versions and internal hostnames.
  • Error responses that print stack traces are a development default that a debug-friendly build often keeps in production.

Check the common paths against your own public URL

You can confirm this on your own app with no attack. From a logged-out session (a fresh private browsing window, or a plain command-line request with no cookies), request the common patterns against your public URL and read what comes back. Do this only against your own app.

Request each of these as a logged-out visitor: /debug, /_debug, /api/test, /api/status, /api/health, /api/__health, /api/env, /api/config, /api/debug, /metrics, /actuator. Then look at the response body and status code for each one that answers.

Watch the responses for the difference between a minimal health check and an information-disclosing one. Both may live at /api/health. The first answers 200 OK. The second answers with build version, database status, environment name, and a list of dependencies. The path is the same; the response is what decides whether it is safe.

  • Suspicious result: the response contains environment values, an API key or connection string, a stack trace with file paths, dependency or framework version numbers, internal hostnames, feature-flag names, or a JSON dump of internal data. Any of those from a logged-out request is a problem.
  • Safer result: the path returns 404, returns a 401 or 403 (it exists but requires auth), or returns a minimal 200 OK with no internal detail. A health check that responds with only ok or 200 and nothing else is the intended shape.
  • Trigger an error on purpose too. Request a malformed URL or a bad payload and confirm the error response is a generic message, not a full stack trace. A leaked trace is the same disclosure class as a debug route.

Remove the routes, or gate them, and stop leaking traces

The fix has three parts, and the order matters.

First, remove debug and test routes from the production build entirely. A route that exists only to help you develop should not be deployed. If your framework supports environment-scoped routing, exclude these paths when the build target is production rather than relying on memory to delete them.

Second, for status and health endpoints you genuinely need, decide what each one should expose and to whom. A public uptime check should return 200 OK and nothing more. Anything that reports versions, dependency state, or internal hostnames should be gated behind authentication or restricted to internal networks, not reachable from the public internet. Keep the public health check minimal and put the detailed status behind a login or an internal-only path.

Third, make sure error responses do not leak stack traces in production. Configure the framework to return a generic error message to the client and send the full trace to your logs or monitoring instead. This is a separate setting from the debug routes and is easy to miss because errors are not part of the happy path you tested.

Removing or gating these routes closes the disclosure from those paths. It does not find a custom route your app invented under a name no checklist predicts, and it does not fix authorization elsewhere. An endpoint that requires login but returns another user's data is a separate problem from one that is simply open. Gating a route controls who reaches it, not whether what it returns is correct.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check probes common debug and status path patterns from the public surface and flags routes that return sensitive internal data without authentication. If one of the well-known paths answers a logged-out request with env values, a stack trace, or internal JSON, the scan reports it with severity and fix direction. It checks the paths an outside visitor would try first.

What it cannot do is enumerate every custom route name your app invented. If your build named a debug endpoint something only your prompt would produce, a pattern-based probe will not guess it. Pair the scan with a manual review of your own route list, going down each route and confirming what it returns to a logged-out caller.

A clean Launch Check on the common paths does not prove your app has no exposed debug route. The scan sees the patterns it knows to try; it does not read your route table or your error handler. Treat a flagged route as a launch blocker, and treat a clean result as one signal, not a guarantee. Next step: open your route list, request each non-public route from a logged-out session, and run a focused Launch Check against the public URL before sharing it.

> launch check

Scan the public surface before launch.

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