PT-2026-7000 · Pypi · Dfir-Unfurl
Published
2026-01-29
·
Updated
2026-01-29
CVSS v4.0
9.3
Critical
| Vector | AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
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()readsdebugviaconfig['UNFURL APP'].get('debug'), which returns a string.UnfurlApp. initpasses that string directly toapp.run(debug=unfurl debug, ...).- If
unfurl.iniomitsdebug, the default argument is the string"True". - As a result, debug mode is effectively always on and cannot be reliably disabled via config.
PoC
- Create a local
unfurl.iniwithdebug = Falseunder[UNFURL APP]. - Run the server using
unfurl app(orpython -c 'from unfurl.app import web app; web app()'). - Observe server logs showing
Debug mode: on/Debugger is active!. - The included PoC script
security poc/poc debug mode.py --spawnautomates 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() + "
"
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("
[+] Debug mode spawn check")
print(" Config: debug = False")
if "Debug mode: on" in output or "Debugger is active" in output:
print(" ✅ Debug mode is ON despite debug=False (vulnerable)")
else:
print(" ⚠️ Debug mode not detected in logs (check output below)")
if output.strip():
print("
--- server output (truncated) ---")
print("
".join(output.splitlines()[:15]))
print("--- end ---")
def run remote probe(target: str) -> None:
import requests
print("
[+] Remote debug indicator probe (best-effort)")
print(f" Target: {target}")
# This app does not easily throw exceptions from user input, so
# absence of indicators does NOT prove debug is off.
probe urls = [
f"{target.rstrip('/')}/ nonexistent ",
]
detected = False
for url in probe urls:
try:
resp = requests.get(url, timeout=10)
if "Werkzeug Debugger" in resp.text or "Traceback" in resp.text:
detected = True
print(" ✅ Debug indicators found")
break
except Exception as e:
print(f" ⚠️ Probe failed: {e}")
if not detected:
print(" ⚠️ No debug indicators found (this is not definitive)")
def main():
parser = argparse.ArgumentParser(description='Unfurl debug mode PoC (corrected)')
parser.add argument('--spawn', action='store true', help='Run local spawn check (default)')
parser.add argument('--target', help='Target Unfurl URL for remote probe')
args = parser.parse args()
if args.target:
run remote probe(args.target)
else:
run spawn check()
if name == ' main ':
main()Impact
If 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.
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Dfir-Unfurl