Skip to content

XSS in NiceGUI apps which uses `ui.sub_pages` and render arbitrary user-provided links

Moderate
falkoschindler published GHSA-m7j5-rq9j-6jj9 Jan 8, 2026

Package

pip nicegui (pip)

Affected versions

>=2.22.0,<=3.4.1

Patched versions

3.5.0

Description

Summary

An unsafe implementation in the click event listener used by ui.sub_pages, combined with attacker-controlled link rendering on the page, causes an XSS when the user actively clicks on the link.

Details

  1. On click, eventually sub_pages_navigate event is emitted.

    document.addEventListener("click", (e) => {
    const a = e.target.closest("a[href]");
    if (a && a.target !== "_blank" && !a.hasAttribute("download")) {
    const href = a.getAttribute("href");
    if (href.startsWith("/")) {
    e.preventDefault();
    const currentPath = getCleanCurrentPath();
    const targetUrl = new URL(href, window.location.origin);
    const targetPath = stripPathPrefix(targetUrl.pathname + targetUrl.search);
    // Handle same-page fragment navigation
    if (currentPath === targetPath && targetUrl.hash) {
    if (handleFragmentNavigation(href, targetUrl)) {
    return;
    }
    }
    // Regular page navigation
    emitEvent("sub_pages_navigate", stripPathPrefix(href));
    }
    }
    });

  2. SubPagesRouter (used by ui.sub_pages), lisnening on sub_pages_navigate, _handle_navigate runs.

    class SubPagesRouter:
    def __init__(self, request: Request | None) -> None:
    on('sub_pages_open', lambda event: self._handle_open(event.args))
    on('sub_pages_navigate', lambda event: self._handle_navigate(event.args))

  3. _handle_navigate runs run_javascript with f-string substituting self.current_path which is simply surrounded by double-quotes. The string context can be broken out easily.

async def _handle_navigate(self, path: str) -> None:
# NOTE: keep a reference to the client because _handle_open clears the slots so that context.client does not work anymore
client = context.client
await self._handle_open(path)
if (
not has_any_unresolved_path(client) or # path is handled by `ui.sub_pages`
not self._other_page_builder_matches_path(path, client) # `ui.sub_pages` is still responsible
):
client.run_javascript(f'''
const fullPath = (window.path_prefix || '') + "{self.current_path}";
if (window.location.pathname + window.location.search + window.location.hash !== fullPath) {{
history.pushState({{page: "{self.current_path}"}}, "", fullPath);
}}
''')
else:
client.open(path, new_tab=False)

PoC

The minimal PoC boils down to this:

from nicegui import ui

ui.sub_pages({'/': lambda: ui.link('Go to XSS', '/"+alert(1)+"')})

ui.run()

However, it is more likely that the attack takes place with attacker-controlled input, for which this shows it:

from nicegui import app, ui

ui.sub_pages({'/': lambda: ui.label('Hello, World!')})

ui.textarea('Markdown content').bind_value(app.storage.general, 'markdown_content')

ui.markdown().bind_content_from(app.storage.general, 'markdown_content')

ui.run()

Vulnerable input is [XSS LINK](/"+alert(document.domain)+") (causes double payload execution, though)

Both cases require someone to click on the link.

image

Impact

Any page which uses ui.sub_pages and renders arbitrary links on screen (common case of ui.markdown) is affected.

The impact is low since a click is always required from the user, who can on-hover to discover the sketchy content of the link and stop if well-trained.

Appendix

AI is used safely to judge the CVSS scoring (input is not even provided, just the impact statement).

Please find the results in https://poe.com/s/y5DvyqgtszDGLUuHin1O

Scoring update after manual review

  • Scope Changed is more inline with other posted XSS vulnerabilities
  • Availability None: No DDoS is possible with this. Site remains performant as ever.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

CVE ID

CVE-2026-21872

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Credits