Skip to content

Commit 9b10645

Browse files
1 parent ba7abf0 commit 9b10645

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-vg9h-jx4v-cwx2",
4+
"modified": "2026-01-29T15:32:33Z",
5+
"published": "2026-01-29T15:32:33Z",
6+
"aliases": [],
7+
"summary": "Unfurl's debug mode cannot be disabled due to string config parsing (Werkzeug debugger exposure)",
8+
"details": "### Summary\nThe 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.\n\n### Details\n- `unfurl/app.py:web_app()` reads `debug` via `config['UNFURL_APP'].get('debug')`, which returns a string.\n- `UnfurlApp.__init__` passes that string directly to `app.run(debug=unfurl_debug, ...)`.\n- If `unfurl.ini` omits `debug`, the default argument is the string `\"True\"`.\n- As a result, debug mode is effectively always on and cannot be reliably disabled via config.\n\n### PoC\n1. Create a local `unfurl.ini` with `debug = False` under `[UNFURL_APP]`.\n2. Run the server using `unfurl_app` (or `python -c 'from unfurl.app import web_app; web_app()'`).\n3. Observe server logs showing `Debug mode: on` / `Debugger is active!`.\n4. The included PoC script `security_poc/poc_debug_mode.py --spawn` automates this check.\n\n### PoC Script (inline)\n```python\n#!/usr/bin/env python3\n\"\"\"\nUnfurl Debug Mode PoC (Corrected)\n================================\n\nThis PoC demonstrates that Unfurl's Flask debug mode is effectively\n**always enabled by default** due to string parsing of the `debug`\nconfig value. Even `debug = False` in `unfurl.ini` evaluates truthy\nwhen passed to `app.run(debug=...)`.\n\nTwo modes:\n1) --spawn (default): launch a local Unfurl server with debug=False\n in a temp config and inspect logs for \"Debug mode: on\".\n2) --target: attempt a remote indicator check (best-effort; may be silent\n if no exception is triggered).\n\"\"\"\n\nimport argparse\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport textwrap\nimport time\n\n\ndef run_spawn_check() -> None:\n repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))\n\n ini_contents = textwrap.dedent(\"\"\"\n [UNFURL_APP]\n host = 127.0.0.1\n port = 5055\n debug = False\n remote_lookups = false\n\n [API_KEYS]\n bitly =\n macaddress_io =\n \"\"\").strip() + \"\\n\"\n\n with tempfile.TemporaryDirectory() as tmp:\n ini_path = os.path.join(tmp, 'unfurl.ini')\n with open(ini_path, 'w') as f:\n f.write(ini_contents)\n\n env = os.environ.copy()\n env['PYTHONPATH'] = repo_root\n\n cmd = [sys.executable, '-c', 'from unfurl.app import web_app; web_app()']\n proc = subprocess.Popen(\n cmd,\n cwd=tmp,\n env=env,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True\n )\n\n # Allow server to start and emit logs\n time.sleep(2)\n proc.terminate()\n try:\n out, err = proc.communicate(timeout=2)\n except subprocess.TimeoutExpired:\n proc.kill()\n out, err = proc.communicate()\n\n output = (out or \"\") + (err or \"\")\n\n print(\"\\n[+] Debug mode spawn check\")\n print(\" Config: debug = False\")\n\n if \"Debug mode: on\" in output or \"Debugger is active\" in output:\n print(\" ✅ Debug mode is ON despite debug=False (vulnerable)\")\n else:\n print(\" ⚠️ Debug mode not detected in logs (check output below)\")\n\n if output.strip():\n print(\"\\n--- server output (truncated) ---\")\n print(\"\\n\".join(output.splitlines()[:15]))\n print(\"--- end ---\")\n\n\ndef run_remote_probe(target: str) -> None:\n import requests\n\n print(\"\\n[+] Remote debug indicator probe (best-effort)\")\n print(f\" Target: {target}\")\n\n # This app does not easily throw exceptions from user input, so\n # absence of indicators does NOT prove debug is off.\n probe_urls = [\n f\"{target.rstrip('/')}/__nonexistent__\",\n ]\n\n detected = False\n for url in probe_urls:\n try:\n resp = requests.get(url, timeout=10)\n if \"Werkzeug Debugger\" in resp.text or \"Traceback\" in resp.text:\n detected = True\n print(\" ✅ Debug indicators found\")\n break\n except Exception as e:\n print(f\" ⚠️ Probe failed: {e}\")\n\n if not detected:\n print(\" ⚠️ No debug indicators found (this is not definitive)\")\n\n\ndef main():\n parser = argparse.ArgumentParser(description='Unfurl debug mode PoC (corrected)')\n parser.add_argument('--spawn', action='store_true', help='Run local spawn check (default)')\n parser.add_argument('--target', help='Target Unfurl URL for remote probe')\n args = parser.parse_args()\n\n if args.target:\n run_remote_probe(args.target)\n else:\n run_spawn_check()\n\n\nif __name__ == '__main__':\n main()\n```\n\n### Impact\nIf the service is exposed beyond localhost (bound to 0.0.0.0 or reverse-proxied), an attacker can access the Werkzeug debugger. This can disclose sensitive information and may allow remote code execution if a debugger PIN is obtained. At minimum, stack traces and environment details are exposed on errors.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V4",
12+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "PyPI",
19+
"name": "dfir-unfurl"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "20250810"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/obsidianforensics/unfurl/security/advisories/GHSA-vg9h-jx4v-cwx2"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/obsidianforensics/unfurl/commit/4c0a07ab1e9af3a1ddf0e7f47153ec9ba77946dd"
44+
},
45+
{
46+
"type": "PACKAGE",
47+
"url": "https://github.com/obsidianforensics/unfurl"
48+
}
49+
],
50+
"database_specific": {
51+
"cwe_ids": [
52+
"CWE-489"
53+
],
54+
"severity": "CRITICAL",
55+
"github_reviewed": true,
56+
"github_reviewed_at": "2026-01-29T15:32:33Z",
57+
"nvd_published_at": null
58+
}
59+
}

0 commit comments

Comments
 (0)