The admin panel you left unlocked

Stack traces in production: what your error pages give away

Every app errors sometimes. A bad query, a null where you expected an object, a route that takes a parameter you didn't sanitize. Locally, that's a gift: a rich error page with the traceback, the variables, and an interactive console to poke at the failure.

Now picture that same page rendering on production, except the person who tripped it isn't you. They appended a single quote to a URL to see what would happen. What does the page show them? On a lot of sites the answer is: your source, your env, your secrets. The detailed error page that saves you hours in development becomes a confession in production, handed to whoever sends the request that breaks you.

The pages built to help you locally

Most stacks ship a developer-friendly error handler that's only supposed to run in development.

Flask uses Werkzeug, whose debugger renders a traceback with every frame expandable and, worse, an interactive console pinned behind a PIN. Laravel and other PHP apps use Whoops, which prints a polished error page showing the failing code, the surrounding lines, the request data, and the environment. Rails in development mode shows its own detailed error page with source extracts and request parameters. Django, with DEBUG = True, renders a full traceback plus the settings. They all share one fault line: the thing that makes them useful, showing you everything, is exactly what makes them dangerous when "everything" includes production secrets and the request comes from outside.

The leak happens because the environment flag didn't flip. FLASK_DEBUG=1 or app.run(debug=True) left on. APP_DEBUG=true in Laravel's .env. RAILS_ENV not set to production. DEBUG = True in Django. Each is a one-line mistake, and each is invisible until something errors.

What the page prints

The detail that helps you debug is the same detail an attacker wants for reconnaissance. One page, two readers.

A triggered Werkzeug, Whoops, or Rails error page typically shows the exact file and line that failed, a chunk of your actual source code around it, the values of local variables at the point of failure, the full request including headers and body, and very often the environment variables. That environment dump is the prize: DATABASE_URL with the password in it, API keys, the framework's signing secret. The signing secret matters more than it looks, because in several frameworks it's what signs session cookies, so leaking it can let an attacker forge a valid admin session without ever guessing a password. And the source extract tells them how your code works, which is the head start for finding the next bug.

  • Source code around the failure, so they can read your logic.
  • Local variable values, which can include tokens, user data, or query results mid-flight.
  • Environment variables: database credentials, API keys, the session-signing secret.
  • The full request and stack, which maps your framework, versions, and dependencies.
request
GET /user/abc?id=' HTTP/1.1 Host: yoursite.com
response
HTTP/1.1 500 Internal Server Error
Whoops \ ErrorException
at app/Http/Controllers/UserController.php:42
DB_PASSWORD => prod_S3cr3t_2024
APP_KEY => base64:9f2c1e7b4a8d...
request: { id: "'" }
A single malformed parameter triggers the error page, and the error page prints the database password and app key.

How a stranger gets your app to error on purpose

They don't wait for a natural crash. They cause one. It's trivial.

Append a single quote to a query parameter to provoke a SQL error. Pass a string where a number is expected. Request an object ID that doesn't exist. Send malformed JSON. Hit a route with a missing required parameter. Any of these can throw an unhandled exception, and if debug mode is on, the framework answers a 500 with the full developer error page instead of a generic one. Automated scanners do this constantly, fuzzing parameters across endpoints precisely to see which ones return a verbose traceback. A Werkzeug debugger left exposed is the sharpest case: if they can reach the interactive console and crack or brute the PIN, they have remote code execution, not just a leak. This is the active-page sibling of an exposed Symfony profiler or Django debug toolbar; there the panel sits at a fixed path, here the error itself is the door.

0:00Scanner fuzzes a parameter
0:05Endpoint returns a 500 traceback
0:20Reads DB creds and app key from env dump
0:30Forges an admin session
From a malformed request to a forged session in under a minute, no password involved.

Turn debug off, and prove it from outside

The fix is to make production return a generic error page, never a debug one, and to set that in the deploy environment rather than a file you hope got copied.

Set the production flag explicitly for your stack: FLASK_DEBUG=0 and never pass debug=True to app.run, APP_DEBUG=false in Laravel, DEBUG = False in Django, and ensure RAILS_ENV=production for Rails. Then add a custom 500 handler that returns a plain "something went wrong" page and logs the real detail server-side where only you can read it. The user gets nothing useful; you get the full trace in your logs.

# Flask
FLASK_DEBUG=1          # Werkzeug debugger to anyone

# Laravel .env
APP_DEBUG=true         # Whoops prints env on every 500
The fix is one flag per stack, plus a generic error page that logs detail privately.

Checking this from the outside takes seconds: send a deliberately malformed request to a couple of endpoints and look at what comes back. A styled traceback with file paths is a fail; a plain 500 is a pass. An error page that dumps your APP_KEY is a credential leak anyone can trigger on purpose, and it's the kind of thing you only catch by looking at the response a stranger would get. That's exactly the view SurfaceCheckr takes. It sends the same harmless malformed request, reads the same 500 you'd see from a coffee shop wifi, and flags any route returning a debug error page or an exposed Werkzeug console. It reads what's served and stops there, so treat it as the ten-second check that your secrets aren't printed on the public internet, not as a substitute for a real pentest of your application logic.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.