# PYTHON-FLASK-001: Flask Debug Mode Enabled

> **Severity:** HIGH | **CWE:** CWE-489 | **OWASP:** A05:2021

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-001
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-001 --project .`

## Description

This rule detects Flask applications that call app.run(debug=True), enabling the interactive
Werkzeug debugger. When debug mode is active, any unhandled exception opens an interactive
Python console directly in the browser. An attacker who can trigger a 500 error -- by sending
malformed input, hitting an edge case, or guessing a route -- gains the ability to execute
arbitrary Python code in the server process. This is unauthenticated remote code execution,
not a theoretical risk.

Beyond the debugger itself, debug mode also exposes full stack traces with local variable
values in HTTP responses, enables the auto-reloader, and disables several Flask security
hardening behaviors. Stack traces leak framework versions, file system paths, configuration
values, and application logic that attackers use to plan further exploitation.

The detection uses FlaskApp.method("run").where("debug", True) -- a QueryType-based precise
match. FlaskApp declares fqns=["flask"], so the engine only matches objects actually imported
from the flask package. The .where("debug", True) filter means only calls where the debug
keyword argument is explicitly the boolean True are flagged. Calls with debug=False,
debug=os.getenv(...), or no debug argument at all are not flagged. This precision delivers
zero false positives on properly configured applications.


## Vulnerable Code

```python
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/users')
def get_users():
    # Some application logic
    return {'users': [...]}

if __name__ == '__main__':
    # DANGEROUS: Debug mode enabled
    app.run(debug=True)  # Vulnerable!

# Also vulnerable:
# app.debug = True
# app.run()

# Or via config:
# app.config['DEBUG'] = True
```

## Secure Code

```python
import os
from flask import Flask

app = Flask(__name__)

@app.route('/api/users')
def get_users():
    return {'users': []}

if __name__ == '__main__':
    # SAFE: Debug mode disabled explicitly
    app.run(debug=False)

    # BETTER: Drive debug mode from an environment variable.
    # Ensure FLASK_DEBUG is never set to 'true' in production.
    debug_mode = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
    app.run(debug=debug_mode)

    # BEST: Use a production WSGI server instead of app.run() entirely.
    # Gunicorn: gunicorn -w 4 -b 0.0.0.0:8000 app:app
    # uWSGI:    uwsgi --http 0.0.0.0:8000 --module app:app
    # Waitress: waitress-serve --port=8000 app:app

```

## Detection Rule (Python SDK)

```python
from codepathfinder.python_decorators import python_rule
from codepathfinder import calls, Or


@python_rule(
    id="PYTHON-FLASK-001",
    name="Flask Debug Mode Enabled",
    severity="HIGH",
    category="flask",
    cwe="CWE-489",
    cve="CVE-2015-5306",
    tags="python,flask,debug-mode,configuration,information-disclosure,OWASP-A05,CWE-489,production,werkzeug,security,misconfiguration",
    message="Flask debug mode enabled. Never use debug=True in production. Use a production WSGI server like Gunicorn.",
    owasp="A05:2021",
)
def detect_flask_debug_mode():
    """
    Detects Flask applications with debug mode enabled.

    Matches:
    - app.run(debug=True)
    - *.run(debug=True)

    Example vulnerable code:
        app = Flask(__name__)
        app.run(debug=True)  # Detected!
    """
    from codepathfinder import QueryType

    class FlaskApp(QueryType):
        fqns = ["flask"]

    return FlaskApp.method("run").where("debug", True)
```

## How to Fix

- Remove debug=True from all app.run() calls. The default is False -- omitting the argument is safe and explicit.
- Drive debug mode through an environment variable (FLASK_DEBUG) and verify it is set to 0 or false in every production, staging, and CI environment.
- Replace app.run() with a production WSGI server such as Gunicorn, uWSGI, or Waitress. These servers never expose the Werkzeug debugger regardless of configuration.
- Implement a global exception handler (app.errorhandler(500)) that returns a generic error page to users and logs the full traceback server-side only.
- Add a pre-deployment check in CI -- run this rule with pathfinder ci -- to catch accidental re-introduction of debug=True before it reaches production.

## Security Implications

- **Unauthenticated Remote Code Execution via Werkzeug Debugger:** The Werkzeug interactive debugger opens a Python console on every unhandled exception.
Any visitor who can trigger a 500 error can execute arbitrary Python in the server
process. There is a PIN mechanism in Werkzeug 0.11+, but it is derived from machine
details (hostname, MAC address, process ID, app path) that are frequently leaked through
stack traces or accessible via /proc on Linux. Do not treat the PIN as a security control.

- **Full Stack Traces Leaked in HTTP Responses:** Debug mode returns detailed exception tracebacks to the browser, including local variable
values, function call chains, and source file paths. Attackers use this to map the
application's internal structure, identify library versions with known CVEs, and locate
exact lines of code to target with follow-up attacks.

- **Secret and Configuration Disclosure:** Stack traces frequently capture the values of configuration variables, database
connection strings, API keys, and Flask's SECRET_KEY at the moment of the exception.
A single triggered error can expose credentials that give an attacker direct database
access or the ability to forge session cookies.

- **Persistent Exposure Through Reloader:** The debug reloader watches the file system for source changes and restarts the app
automatically. In containerized environments with mounted volumes, this can expose
file system paths, trigger import errors that produce verbose stack traces, and create
timing side-channels that reveal the application's directory structure.


## FAQ

**Q: Why is debug=True dangerous if Werkzeug has a PIN protecting the debugger?**

The Werkzeug debugger PIN is computed from the machine's hostname, the first network
interface's MAC address, the process ID, and the app's root path. These values are
frequently leaked through stack traces, accessible via /proc on Linux containers, or
guessable on shared hosting. Researchers have demonstrated PIN bypasses against real
deployments. The PIN is a speed bump, not a security boundary.


**Q: How does this rule know the app object is from Flask and not some other library?**

The QueryType FlaskApp declares fqns=["flask"], so the engine traces the import of the
object back to the flask package. A custom class with a run(debug=True) method that is
not imported from flask will not be flagged. This import-aware matching is what makes the
.where() approach produce zero false positives.


**Q: My local development setup uses app.run(debug=True). Will that be flagged?**

Yes. The rule cannot distinguish development files from production code without path-based
filtering. You can suppress findings in local scripts by adding a path exclusion in your
Code Pathfinder configuration, or by switching local development to use Flask's built-in
test client (app.test_client()) which never starts the HTTP server.


**Q: What is the difference between FLASK_DEBUG=1 in the environment and debug=True in code?**

FLASK_DEBUG is an environment variable Flask reads at startup. This rule catches debug=True
in the Python source code -- a hardcoded value. If you rely solely on environment variables,
this rule will not flag your code, but you still need to ensure FLASK_DEBUG is not set to 1
in production. Use environment variable auditing as a complementary control.


**Q: How do I run this rule in CI/CD?**

Run: pathfinder ci --ruleset python/flask/PYTHON-FLASK-001 --project .
The rule outputs SARIF, JSON, or CSV. On GitHub Actions it can post inline pull request
comments pointing to the exact line where debug=True appears, blocking the merge before
the misconfiguration reaches production.


**Q: Does this rule catch app.config["DEBUG"] = True as well?**

No. This rule specifically matches the app.run(debug=True) call-site pattern. Configuration
set via app.config["DEBUG"] = True is a separate pattern not covered here. For comprehensive
debug-mode coverage, combine this rule with a configuration assignment audit rule.


**Q: What WSGI servers should I use instead of app.run()?**

Gunicorn (gunicorn app:app) is the most common choice. uWSGI, Waitress (pure Python, works
on Windows), and Hypercorn (supports ASGI) are also solid options. All of them ignore
Flask's debug setting -- the Werkzeug development server that app.run() starts is never
invoked in these deployments.


## References

- [CWE-489: Active Debug Code](https://cwe.mitre.org/data/definitions/489.html)
- [CVE-2015-5306: Werkzeug Debug PIN Weakness](https://nvd.nist.gov/vuln/detail/CVE-2015-5306)
- [Flask Security Considerations](https://flask.palletsprojects.com/en/stable/security/)
- [Flask Deploying to Production](https://flask.palletsprojects.com/en/stable/deploying/)
- [Werkzeug Debugger Documentation](https://werkzeug.palletsprojects.com/en/stable/debug/)
- [OWASP A05:2021 Security Misconfiguration](https://owasp.org/Top10/A05_2021-Security_Misconfiguration/)

---

Source: https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-001
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
