Debug mode: five frameworks, five different exposures
“Turn off debug mode in production” is the most repeated line in this subject, and it
hides how differently the five frameworks behave when you do not. The same unhandled
ValueError, from the same shape of view, produces responses ranging from 21 bytes
to 47,207 bytes — and in one case something that is not a response page at all.
Everything below except the Spring Boot row was measured in a single session: identical exception, and a local variable holding a canary value in scope at the moment of failure, so that “leaks a traceback” and “leaks your data” could be told apart. Spring Boot was read at source; there is no JVM on this machine, and that row is marked.
Measured
Section titled “Measured”| Framework | Setting | Response | Traceback in body | Local values in body | Interactive console |
|---|---|---|---|---|---|
| FastAPI | debug=False — the default |
21 B | no | no | no |
| Django | DEBUG=False |
145 B | no | no | no |
| Express | NODE_ENV=production |
148 B | no | no | no |
| Flask | debug=False — the default |
265 B | no | no | no |
| Spring Boot | defaults (read at source) | error page, no trace | no | no | no |
| Express | NODE_ENV unset — the default |
1,703 B | yes | no | no |
| FastAPI | debug=True |
4,899 B | yes | no | no |
| Flask | debug=True |
12,357 B | yes | no | yes |
| Django | DEBUG=True |
47,207 B | yes | yes | no |
Express is the only one that leaks by default
Section titled “Express is the only one that leaks by default”Four of these frameworks default to safe. Express does not, and the reason is a piece of configuration nobody thinks of as a security setting.
Express reads process.env.NODE_ENV || 'development', and its error handler sends
err.stack unless that value is exactly production. So an application deployed
without NODE_ENV set — which is the state of a bare container, a systemd unit, or a
node server.js on a VM — returns a full stack trace to anyone who can provoke an
error. Measured: 1,703 bytes with the stack, versus 148 bytes once NODE_ENV=production
is set.
Nothing warns about this. There is no debug flag to forget; the exposure comes from an environment variable being absent.
Django’s framework default is not the default you have
Section titled “Django’s framework default is not the default you have”Django’s shipped default is DEBUG = False — django.conf.global_settings says so.
That is not the value in your project.
django-admin startproject writes DEBUG = True into the generated settings.py,
along with ALLOWED_HOSTS = []. So every Django project begins life in the most
exposed state in this table, and the framework default never applies to a real
application. When people say “Django is safe by default” and “Django leaks everything in
debug”, both are true and they are talking about different defaults.
Two different worst cases, and neither is simply worse
Section titled “Two different worst cases, and neither is simply worse”The bottom two rows are the interesting ones, because they fail in ways that are not comparable.
Django leaks the most data. 47,207 bytes containing the traceback, the local
variables of every frame — the canary value appears in the response body — and a
settings dump. Being precise about that dump: Django sanitizes it. The
SECRET_KEY value did not appear; the page shows ******** in its place. So “Django
dumps your settings” overstates it. What Django genuinely hands over is the local
variable values of your own code, which is frequently worse — that is where the record
being processed, the token being validated, and the query parameters actually live.
Flask hands over the most capability. The Werkzeug debugger page is smaller, 12,357 bytes, and notably it does not contain local values inline. What it contains is an interactive console. The exposure is not disclosure at all — it is code execution, in your process, with your credentials. A smaller response carrying strictly more risk.
So the ranking depends on the question. By bytes disclosed, Django. By what an attacker can do, Flask, and it is not close.
The safe end
Section titled “The safe end”FastAPI’s 21 bytes is the whole response — Internal Server Error as plain text.
debug=True gives a 4,899-byte traceback and, unlike Django, no local values, which
makes it the mildest of the debug modes here.
Spring Boot is the only one whose safe behaviour is a documented decision rather than
a default that happens to be off. spring.web.error.include-stacktrace defaults to
never and include-message likewise, both since Boot 2.3 tightened them deliberately.
The value worth knowing is the third option: on-param adds the stack trace when a
request parameter asks for it — client-triggered disclosure, not a middle setting.
An application running on-param is one query string away from the Express row.
What to take from this
Section titled “What to take from this”- Check
NODE_ENVon every Node deployment. It is the only default in this table that leaks, and its absence looks like nothing. - Never read a framework’s documented default as your project’s value. Django is the
clear case, and the check is
grep DEBUG settings.py, not the docs. - Distinguish a traceback from local values. Django and FastAPI both “show a traceback” in debug; only one of them includes the data your code was holding.
- Treat Flask’s debug mode as remote code execution, not as verbose errors, and never as something that can be left on behind an internal-only URL.
- Check for
on-paramin Spring configurations. It is the one setting here that lets the client decide how much it gets told.
Related
Section titled “Related”- Django debug in production — the 47,207-byte row in its own cluster
- The Werkzeug debugger console — how the PIN on that console is derived
- FastAPI debug and tracebacks — the mildest of the debug modes
- Express error handling —
NODE_ENVand the four-argument handler - Spring Boot error details —
on-paramand the renamed property prefix