Skip to content
Information DisclosureDebug Mode

Debug Mode in Production: Consoles, Profilers and Dev Servers on the Open Internet

A debug flag or a development server that reaches production turns developer tooling into an attack surface: interactive consoles, profilers that record every request, and servers that read files straight off disk. Here is how it happens, what it exposes, and how to ship a build that cannot carry it.

Part 2 of 3From the Information Disclosure series

At a glance

CWE-489
Interpreter
Framework debug tooling and development servers running on a reachable host
Required condition
A deployment runs with its framework debug flag on, or a development server listens on a routable interface.
Potential impact
Code execution through debug consoles, leaked secrets and recorded requests, and file reads through the dev server.
Primary defense
Deploy production builds with debug off, development packages uninstalled and dev servers bound to loopback.

Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.

On this page

Debug mode assumes only the developer is watching. Framework debug flags render interactive error pages, record requests for later inspection and show the configuration the process started with. Development servers push hot-reload updates and serve files straight from the project directory. On localhost that is convenience; on a reachable network it works for whoever connects.

MITRE files this under CWE-489, Active Debug Code, and OWASP Top 10:2025 maps it to A02 Security Misconfiguration. There is no faulty line of code to patch: the deployment is in the wrong state.

How it happens

The development command becomes the start command. A container runs npm run dev, flask run --debug or php artisan serve because that worked on a laptop, with --host 0.0.0.0 added so the port mapping could reach it.

One environment file serves every stage. APP_DEBUG=true or RAILS_ENV=development is copied into production, or a staging host with real data is reachable from the internet.

Debug packages ship with the release. Laravel Debugbar, Clockwork, Ignition and Symfony's WebProfilerBundle register routes whenever they are installed and enabled, so one flag is all that separates them from the internet.

Surface Typical path What it hands out
Werkzeug debugger console /console Python execution, behind a PIN
Laravel Ignition /_ignition/health-check Routes that can run code
Symfony profiler /_profiler Recorded requests, queries, server parameters
Laravel Debugbar, Clockwork /_debugbar/open, /__clockwork/app Stored requests and sessions
Rails info pages /rails/info/properties Versions and the route map
Vite, Next.js, webpack-dev-server /@vite/client, /_next/webpack-hmr Source files and source maps
Editor-launch middleware /__open-in-editor A process started on the host

A concrete walk-through: two requests, two verdicts

An authorized tester asks a staging host for the client script only a Vite dev server serves:

GET /@vite/client HTTP/1.1
Host: staging.shop.example
HTTP/1.1 200 OK
Content-Type: text/javascript

...
export { ErrorOverlay, createHotContext, injectQuery, removeStyle, updateStyle };

A production build has no /@vite/ routes, so createHotContext means the dev server itself is answering. The API host gets one request too:

GET /console HTTP/1.1
Host: api.staging.shop.example
<title>Console // Werkzeug Debugger</title>
...
<h3>Console Locked</h3>

That is the Werkzeug debugger's Python console. The assessment stops here: no PIN guesses, no expressions, no file paths. The markers prove the state.

What an attacker gains

  • Code execution. Werkzeug's documentation says the debugger "allows the execution of arbitrary code." Its PIN is derived from host values such as the user name, module path and machine identifiers, so a path traversal read can expose its inputs, and Werkzeug states the PIN "is not meant to entirely secure the debugger." Ignition's solution runner became unauthenticated remote code execution in CVE-2021-3129 on Laravel sites in debug mode.
  • Other users' sessions. The Symfony profiler, Debugbar and Clockwork keep recent requests with cookies, form posts and queries.
  • Configuration secrets. Profilers and debug error pages can show values such as APP_SECRET or DATABASE_URL, enough to forge tokens or reach the database.
  • Source and files. A dev server serves untransformed modules and source maps; see exposed secrets in JavaScript. Vite dev servers before 6.2.6, 6.1.5, 6.0.15, 5.4.18 and 4.5.13 also carry unauthenticated file-read bugs (CVE-2025-30208, CVE-2025-31125, CVE-2025-31486, CVE-2025-32395), reachable only because the server listens on the network.
  • Process launch. Editor-launch middleware takes a file path from the query string and starts an editor on the host.

Reading a SelfSec finding

After the crawl, the Development Mode Exposure module requests a fixed catalog of paths at the site's origin: dev-server paths when the crawl saw a front-end framework, debug-tool paths when it saw a server runtime such as PHP, Python, Rails, Java, ASP.NET or Node.js, and both when nothing was identified.

  • A plain 200 never confirms. The response must carry the tool's own marker, such as createHotContext, Console Locked or can_execute_commands, stay within a size limit, arrive as text/event-stream, or return the error status the tool gives a request missing its parameters. It must also differ from a random path in the same directory, so a single-page-app catch-all is not mistaken for a dev server.
  • Editor-launch endpoints are confirmed from that missing-parameter error; no file path is sent, so no editor starts.
  • Signals become one finding per tool for the whole site. Evidence lists each path, status and marker, such as /_ignition/health-check responded 200 carrying 'can_execute_commands'; replay it with a plain GET.
  • The Werkzeug console, Ignition and editor-launch endpoints are critical (CVSS 9.3); dev servers, the Symfony profiler, Debugbar, Clockwork and Rails info pages are high (CVSS 8.8). All carry CWE-489 and A02:2025 Security Misconfiguration.
  • Spring Boot Actuator, Laravel Telescope and /server-status are probed in the same pass. When they answer they join the scanned surface, but they do not raise this finding. Django's DEBUG page and ASP.NET Core's developer exception page appear only on errors; see information disclosure.

The fix: ship a build, not a development process

Build the front end in CI and serve the output from a web server; this image contains no Node.js at all:

FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html

Locally, keep Vite on its default loopback host and drop any --host flag or server.host: true. Next.js deploys next build and next start, never next dev. In Python, run a production WSGI server behind the reverse proxy; Gunicorn never installs the Werkzeug debugger. Leave FLASK_DEBUG unset and never wrap the app in DebuggedApplication:

gunicorn --workers 4 --bind 127.0.0.1:8000 "shop:create_app()"

In Laravel, production turns debug off and the install leaves require-dev packages such as Debugbar and Ignition out:

APP_ENV=production
APP_DEBUG=false
composer install --no-dev --optimize-autoloader
php artisan config:cache

Symfony uses APP_ENV=prod and APP_DEBUG=0, with WebProfilerBundle registered for dev and test only in config/bundles.php. Rails adds the /rails/info routes only in development, so RAILS_ENV=production removes them.

If any surface was reachable, rotate every credential and key it could have shown and review logs for the exposure window. After a reachable console, Ignition or editor-launch endpoint, treat the host as compromised until the logs say otherwise.

Fixes that do not hold

  • Blocking /console or /_profiler at the proxy: prefixes are configurable, and the flag still leaks through error pages.
  • Relying on the Werkzeug PIN: it slows guessing; it does not make a public console safe.
  • Turning debug off without rotating: the keys a profiler showed still work.
  • Calling it only staging: real data and credentials make it production.

A developer review checklist

  1. Confirm APP_DEBUG=false, APP_ENV=prod or production, RAILS_ENV=production, NODE_ENV=production and an unset FLASK_DEBUG.
  2. Read each container's start command: vite, next dev, webpack serve, flask run and php artisan serve do not belong there.
  3. Install with --no-dev or the production dependency group.
  4. Request /console, /_profiler, /_ignition/health-check and /@vite/client on every deployed host and expect the normal not-found response.
  5. Rotate secrets and review logs after any exposure.

References

SelfSec scanner

Find Information Disclosure in your own app

Reading about the class is one thing. SelfSec tests for it against an application you are authorized to assess, and only calls it real when independent evidence agrees.

Find it Confirmed, not guessed 21 vulnerability modules, with the classical engine owning the verdict. Prove it Evidence you can re-read Every finding keeps the raw request, the response and the baseline it was measured against.
Join the launch list