CVE-2026-40035
Unfurl - Werkzeug Debugger Exposure via String Config Parsing
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
9.3
Unfurl - Werkzeug Debugger Exposure via String Config Parsing
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
- Record
CVE-2026-40035- CVSS 4.0
- 9.3 Critical
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 - CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-489
- Updated
- 2026-11-02
EUVDEUVD-2026-20777
9.3
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
- Record
EUVD-2026-20777- CVSS 4.0
- 9.3 Critical
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-92wx-pxch-p9px
9.3
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that...
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
- Record
GHSA-92wx-pxch-p9px- CVSS 4.0
- 9.3 Critical
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-489
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-vg9h-jx4v-cwx2
9.3
Unfurl's debug mode cannot be disabled due to string config parsing (Werkzeug debugger exposure)
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.__init__passes 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)
#!/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 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("\n--- server output (truncated) ---")
print("\n".join(output.splitlines()[:15]))
print("--- end ---")
def run_remote_probe(target: str) -> None:
import requests
print("\n[+] 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")
…
- Record
GHSA-vg9h-jx4v-cwx2- CVSS 4.0
- 9.3 Critical
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 - Weakness
- CWE-489
- Updated
- 2026-11-02
NVD
9.3
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
- Record
CVE-2026-40035- CVSS 4.0
- 9.3 Critical
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.1 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-489
- Updated
- 2026-11-02
OSV
9.3
Unfurl - Werkzeug Debugger Exposure via String Config Parsing
Unfurl through 2025.08 contains an improper input validation vulnerability in config parsing that enables Flask debug mode by default. The debug configuration value is read as a string and passed directly to app.run(), causing any non-empty string to evaluate truthy, allowing attackers to access the Werkzeug debugger and disclose sensitive information or achieve remote code execution.
- Record
CVE-2026-40035- CVSS 4.0
- 9.3 Critical
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 - Weakness
- CWE-489
- Updated
- 2026-11-02
OSVGHSA-vg9h-jx4v-cwx2
9.3
Unfurl's debug mode cannot be disabled due to string config parsing (Werkzeug debugger exposure)
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.__init__passes 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)
#!/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 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("\n--- server output (truncated) ---")
print("\n".join(output.splitlines()[:15]))
print("--- end ---")
def run_remote_probe(target: str) -> None:
import requests
print("\n[+] 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")
…
- Record
GHSA-vg9h-jx4v-cwx2- CVSS 4.0
- 9.3 Critical
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 - Weakness
- CWE-489
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
dfir-unfurlPyPI |
through 20250810 | None known |
dfir-unfurlPyPI |
through 20250810, plus listed versions 202006292020063020200630.220200630.320200630.4 Show 21 moreShow less202007022020070320200729202008122020110220210308202103092021031020210311202106152022020020221100202309002023090120240625202406262024062720241120202411212025021820250312 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-40035 is a critical vulnerability (CVSS 9.3) in Obsidianforensics Unfurl, published January 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.57%, the 47th percentile. No source names a fixed version.
References
github.com/obsidianforensics/unfurl/security/advisories/GHSA-vg9h-jx4v-cwx2
vulncheck.com/advisories/dfir-unfurl-werkzeug-debugger-exposure-via-string-config-parsing
nvd.nist.gov/vuln/detail/CVE-2026-40035
github.com/advisories/GHSA-92wx-pxch-p9px
github.com/obsidianforensics/unfurl/commit/4c0a07ab1e9af3a1ddf0e7f47153ec9ba77946dd
github.com/advisories/GHSA-vg9h-jx4v-cwx2
github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40035.json
github.com/obsidianforensics/unfurl
Left out here: the rest of 2 source descriptions. The JSON record has them.