CVE-2026-21872
Cross-Site Scripting in NiceGUI ui.sub_pages via crafted link click.
- CVSS 6.1
- CWE-79
- Input Validation and Sanitization
- Remote
NiceGUI is a Python-based UI framework. From versions 2.22.0 to 3.4.1, an unsafe implementation in the click event listener used by ui.sub_pages, combined with attacker-controlled link rendering on the page, causes XSS when the user actively clicks on the link. This issue has been patched in version 3.5.0.
- CWE
- CWE-79
- CVSS base score
- 6.1
- Published
- 2026-01-08
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Cross-Site Scripting (XSS)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- NiceGUI
- Fixed by upgrading
- Yes
Solution
Upgrade NiceGUI to version 3.5.0 or later.
Vulnerable code sample
import uvicorn
from fastapi import FastAPI, Request
from nicegui import app, ui
# This code represents a vulnerable implementation similar to the one described
# in CVE-2026-21872 for NiceGUI versions prior to 3.5.0.
# The actual CVE number is fictional, but the vulnerability pattern is plausible.
# The vulnerability arises because the `ui.sub_pages` component does not sanitize
# the paths provided to it. If an attacker can control one of these paths,
# they can inject a 'javascript:' URI, leading to XSS when a user clicks the link.
def setup_routes(app_instance):
@app_instance.get("/")
async def home(request: Request):
return await main_page(request)
@app_instance.get("/page_a")
async def page_a(request: Request):
return await main_page(request, content="<p>This is Page A.</p>")
@app_instance.get("/page_b")
async def page_b(request: Request):
return await main_page(request, content="<p>This is Page B.</p>")
async def main_page(request: Request, content: str = None):
# Attacker controls this input via a URL query parameter.
# e.g., http://localhost:8080/?next_page=javascript:alert('XSS%20triggered!')
attacker_controlled_path = request.query_params.get('next_page', '/page_b')
# In a vulnerable version, ui.sub_pages would directly use the values from this
# dictionary as `href` attributes for the generated navigation links.
pages = {
'Page A': '/page_a',
'Innocent-looking Link': attacker_controlled_path,
}
ui.query('body').style('background-color: #f0f0f0;')
with ui.header().classes('justify-between text-white'):
ui.label('Vulnerable Sub-Pages Demo')
# The vulnerability is conceptually located within this ui.sub_pages component.
# It fails to validate that the provided paths are safe before rendering them
# as links, specifically allowing `javascript:` URIs.
ui.sub_pages(pages).classes('w-full')
if content:
ui.html(content).classes('m-4 p-4 bg-white shadow-md rounded-lg')
else:
ui.label('Welcome to the demonstration page.').classes('m-4')
ui.html("""
<div class="m-4 p-4 bg-white shadow-md rounded-lg">
<p>To exploit the vulnerability:</p>
<ol class="list-decimal list-inside">
<li>Construct a malicious URL with a 'javascript:' payload in the 'next_page' query parameter.</li>
<li>
For example:
<a href="/?next_page=javascript:alert('XSS from sub_pages link!')">
/?next_page=javascript:alert('XSS from sub_pages link!')
</a>
</li>
<li>Send this link to a user.</li>
<li>When the user clicks the "Innocent-looking Link" in the header, the JavaScript payload will execute.</li>
</ol>
</div>
""").classes('text-sm')
# This function call initiates the UI.
return await ui.run_with(app, title="Vulnerable App")
# Setup FastAPI app
fastapi_app = FastAPI()
setup_routes(fastapi_app)
# Mount NiceGUI
app.mount_to(fastapi_app)
if __name__ in {"__main__", "__mp_main__"}:
uvicorn.run(fastapi_app, host="0.0.0.0", port=8080)Patched code sample
from nicegui import ui
import re
# The vulnerability existed because a client-side click handler might
# unsafely use a link's target for navigation, allowing 'javascript:' URIs
# to be executed.
#
# The fix involves ensuring that navigation is handled safely. The most robust
# approach, demonstrated here, is to handle navigation decisions on the
# server-side, where targets can be validated against a whitelist before
# instructing the client to navigate.
# A whitelist of safe URL schemes.
# An internal path (e.g., '/profile') is also considered safe.
SAFE_SCHEMES = {'http', 'https', 'mailto', 'ftp'}
def is_safe_url(url: str) -> bool:
"""
Validates if a URL is safe for redirection.
- Allows relative paths starting with '/'.
- Allows absolute URLs with schemes from a whitelist.
"""
if not isinstance(url, str):
return False
# Allow relative paths which are safe for internal navigation.
if url.startswith('/'):
return True
# Check for a scheme and see if it is in the whitelist.
match = re.match(r'^([a-zA-Z][a-zA-Z0-9+.-]*):', url)
if match:
scheme = match.group(1).lower()
return scheme in SAFE_SCHEMES
# If no scheme and not a relative path, it's considered unsafe.
return False
def safe_navigation_handler(target_url: str):
"""
This function represents the patched, secure navigation handler.
It is called by the on_click event of UI elements. It validates the
URL on the server side before initiating a client-side navigation.
This prevents the execution of malicious 'javascript:' URIs.
"""
if is_safe_url(target_url):
ui.notify(f'SAFE: Navigating to {target_url}', type='positive')
ui.open(target_url)
else:
ui.notify(f'UNSAFE: Blocked navigation to "{target_url}"', type='negative')
# In a real application, this attempt would be logged as a security event.
@ui.page('/')
def main_page():
ui.label('Demonstration of a Fixed XSS Vulnerability').classes('text-2xl')
ui.markdown(
'This example shows a secure implementation that prevents an XSS vulnerability. '
'A vulnerable implementation might directly navigate to a link\'s target on the client side, '
'allowing a `javascript:` URI to execute. '
'The fixed version handles clicks on the server, validating the target URL before navigation.'
)
# This represents a link whose target might be controlled by an attacker
# (e.g., saved in a user's profile and rendered on the page).
malicious_target_from_user_input = "javascript:alert('XSS: This code should not execute!')"
safe_target_internal = '/other_page'
safe_target_external = 'https://nicegui.io'
with ui.card():
ui.label('Click the links below:').classes('font-bold')
# SIMULATED VULNERABLE LINK (NOW SECURED)
# In the vulnerable version, clicking this might have executed the JavaScript.
# In the fixed version, the server-side handler `safe_navigation_handler` is called.
ui.link(
'Clicking this "malicious" link is now safe',
).on('click', lambda: safe_navigation_handler(malicious_target_from_user_input), [])
# SAFE INTERNAL LINK
# This demonstrates that legitimate internal navigation still works.
ui.link(
'Clicking this safe internal link works',
).on('click', lambda: safe_navigation_handler(safe_target_internal), [])
# SAFE EXTERNAL LINK
# This demonstrates that legitimate external navigation also still works.
ui.link(
'Clicking this safe external link works',
).on('click', lambda: safe_navigation_handler(safe_target_external), [])
@ui.page('/other_page')
def other_page():
ui.label('Successfully navigated to a safe internal page.').classes('text-xl')
ui.link('Go back', '/')
ui.run()Payload
/subpage');alert(1);('
Cite this entry
@misc{vaitp:cve202621872,
title = {{Cross-Site Scripting in NiceGUI ui.sub_pages via crafted link click.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-21872},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-21872/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
