NiceGUI has XSS via Code Injection
Description
NiceGUI is a Python-based UI framework. Prior to version 3.8.0, several NiceGUI APIs that execute methods on client-side elements (Element.run_method(), AgGrid.run_grid_method(), EChart.run_chart_method(), and others) use an eval() fallback in the JavaScript-side runMethod() function. When user-controlled input is passed as the method name, an attacker can inject arbitrary JavaScript that executes in the victim's browser. Additionally, Element.run_method() and Element.get_computed_prop() used string interpolation instead of json.dumps() for the method/property name, allowing quote injection to break out of the intended string context. Version 3.8.0 contains a fix.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
niceguiPyPI | < 3.8.0 | 3.8.0 |
Affected products
1- Range: < 3.8.0
Patches
11861f59cc374Merge commit from fork
5 files changed · +77 −26
nicegui/element.py+6 −2 modified@@ -412,7 +412,9 @@ def run_method(self, name: str, *args: Any, timeout: float = 1) -> AwaitableResp """ if not core.loop: return NullResponse() - return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})', timeout=timeout) + return self.client.run_javascript( + f'return runMethod({self.id}, {json.dumps(name)}, {json.dumps(args)})', timeout=timeout, + ) def get_computed_prop(self, prop_name: str, *, timeout: float = 1) -> AwaitableResponse: """Return a computed property. @@ -424,7 +426,9 @@ def get_computed_prop(self, prop_name: str, *, timeout: float = 1) -> AwaitableR """ if not core.loop: return NullResponse() - return self.client.run_javascript(f'return getComputedProp({self.id}, "{prop_name}")', timeout=timeout) + return self.client.run_javascript( + f'return getComputedProp({self.id}, {json.dumps(prop_name)})', timeout=timeout, + ) def ancestors(self, *, include_self: bool = False) -> Iterator[Element]: """Iterate over the ancestors of the element.
nicegui/static/nicegui.js+12 −10 modified@@ -123,19 +123,21 @@ function runMethod(target, method_name, args) { if (typeof target === "object") { if (method_name in target) { return target[method_name](...args); - } else { - return eval(method_name)(target, ...args); } - } - const element = getElement(target); - if (element === null || element === undefined) return; - if (method_name in element) { - return element[method_name](...args); - } else if (method_name in (element.$refs.qRef || [])) { - return element.$refs.qRef[method_name](...args); } else { - return eval(method_name)(element, ...args); + const element = getElement(target); + if (element === null || element === undefined) return; + if (method_name in element) { + return element[method_name](...args); + } else if (method_name in (element.$refs.qRef || [])) { + return element.$refs.qRef[method_name](...args); + } + } + let msg = `Method "${method_name}" not found.`; + if (method_name.includes("=>") || method_name.startsWith("(")) { + msg += " To run arbitrary JavaScript, use ui.run_javascript() instead."; } + logAndEmit("error", msg); } function getComputedProp(target, prop_name) {
tests/test_aggrid.py+15 −9 modified@@ -293,19 +293,25 @@ def page(): screen.should_contain('42') -def test_run_method_with_function(screen: Screen): +def test_run_grid_method_xss(screen: Screen): @ui.page('/') def page(): - grid = ui.aggrid({'columnDefs': [{'field': 'name'}], 'rowData': [{'name': 'Alice'}, {'name': 'Bob'}]}) - - async def print_row(index: int) -> None: - ui.label(f'Row {index}: {await grid.run_grid_method(f"(g) => g.getDisplayedRowAtIndex({index}).data")}') - - ui.button('Print Row 0', on_click=lambda: print_row(0)) + grid = ui.aggrid({ + 'columnDefs': [{'field': 'name'}], + 'rowData': [{'name': 'Alice'}, {'name': 'Bob'}], + }) + ui.button('XSS 1', on_click=lambda: grid.run_grid_method('console.error("X" + "SS")')) + ui.button('XSS 2', on_click=lambda: grid.run_grid_method('x", console.error("X" + "SS"), "y')) + screen.allowed_js_errors.append('Method "console.error("X" + "SS")" not found.') + screen.allowed_js_errors.append('Method "x", console.error("X" + "SS"), "y" not found.') screen.open('/') - screen.click('Print Row 0') - screen.should_contain("Row 0: {'name': 'Alice'}") + screen.click('XSS 1') + screen.click('XSS 2') + screen.wait(1) + assert 'XSS' not in screen.render_js_logs() + screen.assert_py_logger('ERROR', 'Method "console.error("X" + "SS")" not found.') + screen.assert_py_logger('ERROR', 'Method "x", console.error("X" + "SS"), "y" not found.') def test_get_client_data(screen: Screen):
tests/test_element.py+32 −0 modified@@ -241,6 +241,38 @@ def page(): screen.should_contain('<b>Bold 2</b>, `code`, copy&paste, multi\nline') +def test_run_method_xss(screen: Screen): + @ui.page('/') + def page(): + ui.button('XSS 1', on_click=lambda e: e.sender.run_method('console.error("X" + "SS")')) + ui.button('XSS 2', on_click=lambda e: e.sender.run_method('x", console.error("X" + "SS"), "y')) + + screen.allowed_js_errors.append('Method "console.error("X" + "SS")" not found.') + screen.allowed_js_errors.append('Method "x", console.error("X" + "SS"), "y" not found.') + screen.open('/') + screen.click('XSS 1') + screen.click('XSS 2') + screen.wait(1) + assert 'XSS' not in screen.render_js_logs() + screen.assert_py_logger('ERROR', 'Method "console.error("X" + "SS")" not found.') + screen.assert_py_logger('ERROR', 'Method "x", console.error("X" + "SS"), "y" not found.') + + +def test_get_computed_prop_xss(screen: Screen): + @ui.page('/') + def page(): + ui.button('XSS 1', on_click=lambda e: e.sender.get_computed_prop('console.error("X" + "SS")')) + ui.button('XSS 2', on_click=lambda e: e.sender.get_computed_prop('x", console.error("X" + "SS"), "y')) + + screen.allowed_js_errors.append('Method "console.error("X" + "SS")" not found.') + screen.allowed_js_errors.append('Method "x", console.error("X" + "SS"), "y" not found.') + screen.open('/') + screen.click('XSS 1') + screen.click('XSS 2') + screen.wait(1) + assert 'XSS' not in screen.render_js_logs() + + def test_default_props(screen: Screen): @ui.page('/') def page():
website/documentation/content/aggrid_documentation.py+12 −5 modified@@ -250,18 +250,25 @@ def aggrid_run_row_method(): on_click=lambda: grid.run_row_method('Alice', 'setDataValue', 'age', 99)) -@doc.demo('Filter return values', ''' - You can filter the return values of method calls by passing string that defines a JavaScript function. - This demo runs the grid method "getDisplayedRowAtIndex" and returns the "data" property of the result. -''') +@doc.ui def aggrid_filter_return_values(): + ui.link_target('filter_return_values') + + +@doc.demo('Access grid API via JavaScript', ''' + You can access the AG Grid API directly via `ui.run_javascript()` for more complex operations. + This demo accesses the grid API to get the first displayed row's data. +''') +def aggrid_access_api_via_javascript(): grid = ui.aggrid({ 'columnDefs': [{'field': 'name'}], 'rowData': [{'name': 'Alice'}, {'name': 'Bob'}], }) async def get_first_name() -> None: - row = await grid.run_grid_method('g => g.getDisplayedRowAtIndex(0).data') + row = await ui.run_javascript( + f'return getElement({grid.id}).api.getDisplayedRowAtIndex(0).data', + ) ui.notify(row['name']) ui.button('Get First Name', on_click=get_first_name)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-78qv-3mpx-9cqqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27156ghsaADVISORY
- github.com/zauberzeug/nicegui/commit/1861f59cc374ca0dc9d970b157ef3774720f8dbfghsax_refsource_MISCWEB
- github.com/zauberzeug/nicegui/security/advisories/GHSA-78qv-3mpx-9cqqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.