The admin panel you left unlocked

Is your framework's debug toolbar visible to the world?

Open any page on your local Symfony app and there's the profiler bar pinned to the bottom: every query, every route, the request, the session, the full config, one click away. You lean on it daily. It's the best part of working in the framework.

Now go load /_profiler on your production domain. Does the toolbar come back? If it does, the same panel that helps you debug is reading your config out loud to strangers. It was never meant to leave your laptop. But the thing that keeps it there is a single config flag, and a single flag is very easy to get wrong when you're racing to deploy.

What the toolbar is, and why production forgets to remove it

In development, Symfony's web profiler and the Django Debug Toolbar attach to every response. The profiler stores a detailed record of each request and exposes it at /_profiler. Django's toolbar renders inline and only appears when DEBUG = True and your IP is in INTERNAL_IPS.

The intended setup is that these are off in production. Symfony gates the profiler on the APP_ENV and the framework.profiler config, which the prod environment is supposed to disable. Django gates everything on DEBUG. The failure is mundane: someone deploys with APP_ENV=dev because that's what was in the local .env and it got copied. Or DEBUG = True ships because flipping it to False also means configuring ALLOWED_HOSTS and serving static files properly, and that was a problem for later. Later never came. Now the toolbar is live on a public box, and it answers to anyone.

What a stranger reads off it

This is the part that makes it serious. The toolbar is a guided tour of your application's insides, handed to whoever asks.

An exposed Symfony profiler at /_profiler lets a stranger browse stored requests and pull, per request, the full server environment, including environment variables, the loaded configuration, every database query with its parameters, the session contents, and the routing table that maps out every endpoint you have. Environment variables are the worst of it, because that's where database passwords, API keys, and the app secret live. A live Django DEBUG = True does something similar through error pages: trigger any unhandled exception and Django renders a full traceback with local variable values at each frame, the settings module with secrets redacted only by name-guessing, and the complete request. The redaction is shallow. Anything you named in a way Django doesn't recognize as a secret prints in full.

  • Environment variables: DATABASE_URL, APP_SECRET, API keys, mail credentials.
  • Every SQL query with bound parameters, which maps your schema and your data access.
  • The full route list, which is a sitemap of your attack surface.
  • Session data and the request, frame by frame, on any error.
request
GET /_profiler/latest HTTP/1.1 Host: yoursite.com
response
HTTP/1.1 200 OK
Symfony Profiler / Request abc123
Environment:
DATABASE_URL="postgres://app:S3cr3t@db:5432/prod"
APP_SECRET="9f2c1e7b4a..."
Routes: /admin /api/users /api/billing ...
The profiler hands over the database URL, the app secret, and a map of every route, on one unauthenticated request.

How it's found

There's no guesswork. Scanners request /_profiler and /_wdt on every host they sweep, because those are Symfony's fixed profiler paths. For Django, they don't even need a path: they request a URL likely to error, a malformed parameter or a missing object, and read the debug traceback that comes back. The traceback itself announces DEBUG = True and prints the goods. This is the same automated discovery that finds a forgotten phpMyAdmin on the open internet, and it pairs naturally with the broader problem of stack traces leaking in production, where the error page itself is the vulnerability.

The fix is one flag, done properly

The fix is genuinely small, which is the frustrating part: this exposure is pure config, not a code change.

For Symfony, set APP_ENV=prod in your production environment and confirm the profiler bundle is dev-only in composer.json so it isn't even installed in the prod build. For Django, set DEBUG = False and, because that turns off Django's static handling and host checking, set ALLOWED_HOSTS to your real domains at the same time. Do both in your deploy environment, not just in a file you hope got copied correctly.

# Symfony .env that leaked into prod
APP_ENV=dev        # profiler at /_profiler is live

# Django settings.py
DEBUG = True       # full tracebacks to anyone
ALLOWED_HOSTS = []
One flag per framework. Set it in the deploy environment, not a file you assume shipped.

The good news is that this answers in seconds. Request /_profiler on a Symfony host and look at whether the toolbar comes back. Hit a Django route that errors and look at whether you get a styled traceback or a plain 500. Those are public responses, so SurfaceCheckr checks them the way an attacker would, from the outside, across all your hosts, and flags the profiler or debug page if it's live. We don't log in, we don't touch authenticated routes, and we won't tell you anything a pentest would about your business logic. What we will tell you is whether the most sensitive panel in your framework is currently readable by the internet. Given that it leaks your app secret and database password, that's worth two minutes before someone runs the same request you just did.

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.