GitHub Security Advisory: GHSA-vg9h-jx4v-cwx2
Package: dfir-unfurl (PIP)
Severity: CRITICAL (CVSS 0.0)
CWE: CWE-489: Active Debug Code
Vulnerable versions: <= 20250810
Patched in: unpatched

Summary: Unfurl's debug mode cannot be disabled due to string config parsing (Werkzeug debugger exposure)

Details:
### Summary
The Unfurl web app enables Flask debug mode even when configuration sets `debug = False`. The config value is read as a string and passed directly to `app.run(debug=...)`, so any non-empty string evaluates truthy. This leaves the Werkzeug debugger active by default.

### Details
- `unfurl/app.py:web_app()` reads `debug` via `config['UNFURL_APP'].get('debug')`, which returns a string.
- `UnfurlApp.__init__` passes that string directly to `app.run(debug=unfurl_debug, ...)`.
- If `unfurl.ini` omits `debug`, the default argument is the string `"True"`.
- As a result, debug mode is effectively always on and cannot be reliably disabled via config.

### PoC
1. Create a local `unfurl.ini` with `debug = False` under `[UNFURL_APP]`.
2. Run the server using `unfurl_app` (or `python -c 'from unfurl.app import web_app; web_app()'`).
3. Observe server logs showing `Debug mode: on` / `Debugger is active!`.
4. The included PoC script `security_poc/poc_debug_mode.py --spawn` automates this check.

### PoC Script (inline)
```python
#!/usr/bin/env python3
"""
Unfurl Debug Mode PoC (Corrected)
================================

This PoC demonstrates that Unfurl's Flask debug mode is effectively
**always enabled by default** due to string parsing of the `debug`
config value. Even `debug = False` in `unfurl.ini` evaluates truthy
when passed to `app.run(debug=...)`.

Two modes:
1) --spawn (default): launch a local Unfurl server with debug=False
   in a temp config and inspect logs for "Debug mode: on".
2) --target: attempt a remote indicator check (best-effort; may be silent
   if no exception is triggered).
"""

import argparse
import os
import subprocess
import sys
import tempfile
import textwrap
import time


def run_spawn_check() -> None:
    repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))

    ini_contents = textwrap.dedent("""
    [UNFURL_APP]
    host = 127.0.0.1
    port = 5055
    debug = False
    remote_lookups = false

    [API_KEYS]
    bitly =
    macaddress_io =
    """).strip() + "\n"

    with tempfile.TemporaryDirectory() as tmp:
        ini_path = os.path.join(tmp, 'unfurl.ini')
        with open(ini_path, 'w') as f:
            f.write(ini_contents)

        env = os.environ.copy()
        env['PYTHONPATH'] = repo_root

        cmd = [sys.executable, '-c', 'from unfurl.app import web_app; web_app()']
        proc = subprocess.Popen(
            cmd,
            cwd=tmp,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )

        # Allow server to start and emit logs
        time.sleep(2)
        proc.terminate()
        try:
            out, err = proc.communicate(timeout=2)
        except subprocess.TimeoutExpired:
            proc.kill()
            out, err = proc.communicate()

        output = (out or "") + (err or "")

    print("\n[+] Debug mode spawn check")
    print("    Config: debug = False")

    if "Debug mode: on" in output