VYPR
Medium severity6.1OSV Advisory· Published Oct 3, 2025· Updated Apr 15, 2026

CVE-2025-53354

CVE-2025-53354

Description

NiceGUI is a Python-based UI framework. Versions 2.24.2 and below are at risk for Cross-Site Scripting (XSS) when developers render unescaped user input into the DOM using ui.html(). NiceGUI did not enforce HTML or JavaScript sanitization, so applications that directly combine components like ui.input() with ui.html() or ui.chat_message with HTML content without escaping may allow attackers to execute arbitrary JavaScript in the user’s browser. Applications that do not pass untrusted input into ui.html() are not affected. This issue is fixed in version 3.0.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
niceguiPyPI
< 3.0.03.0.0

Affected products

1

Patches

2
4673dc35c94a

Introduce new `sanitize` argument for `ui.html` (#5187)

https://github.com/zauberzeug/niceguiFalko SchindlerOct 1, 2025via ghsa
30 files changed · +377 89
  • examples/chat_with_ai/main.py+2 1 modified
    @@ -1,4 +1,5 @@
     #!/usr/bin/env python3
    +from html_sanitizer import Sanitizer
     from langchain_openai import ChatOpenAI
     from log_callback_handler import NiceGuiLogElementCallbackHandler
     
    @@ -25,7 +26,7 @@ async def send() -> None:
                 response += chunk.content
                 response_message.clear()
                 with response_message:
    -                ui.html(response)
    +                ui.html(response, sanitize=Sanitizer().sanitize)
                 ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)')
             message_container.remove(spinner)
     
    
  • examples/chat_with_ai/requirements.txt+1 0 modified
    @@ -1,3 +1,4 @@
    +html-sanitizer>=2.6.0
     langchain>=0.2
     langchain-community
     langchain_openai
    
  • examples/generate_pdf/main.py+2 1 modified
    @@ -2,6 +2,7 @@
     from io import BytesIO
     
     import cairo
    +from html_sanitizer import Sanitizer
     
     from nicegui import ui
     
    @@ -40,7 +41,7 @@ def update() -> None:
         with ui.column():
             name = ui.input('Name', placeholder='Enter your name', on_change=update)
             email = ui.input('E-Mail', placeholder='Enter your E-Mail address', on_change=update)
    -    preview = ui.html().classes('border-2 border-gray-500')
    +    preview = ui.html(sanitize=Sanitizer().sanitize).classes('border-2 border-gray-500')
         update()
         ui.button('PDF', on_click=lambda: ui.download(generate_pdf(), 'output.pdf')).bind_visibility_from(name, 'value')
     
    
  • examples/generate_pdf/requirements.txt+1 0 modified
    @@ -1,2 +1,3 @@
    +html-sanitizer>=2.6.0
     nicegui>=3.0
     pycairo
    
  • examples/google_one_tap_auth/main.py+1 1 modified
    @@ -25,7 +25,7 @@ def main_page() -> None:
                     data-client_id="{GOOGLE_CLIENT_ID}"
                     data-login_uri="http://localhost:8080/auth">
                 </div>
    -        ''')
    +        ''', sanitize=False)
             ui.label('Sign in with Google One Tap')
             return
     
    
  • examples/svg_clock/main.py+1 1 modified
    @@ -43,7 +43,7 @@ def build_svg() -> str:
         '''
     
     
    -clock = ui.html().classes('self-center')
    +clock = ui.html(sanitize=False).classes('self-center')
     ui.timer(1, lambda: clock.set_content(build_svg()))
     
     ui.run()
    
  • nicegui/elements/chat_message.py+24 10 modified
    @@ -1,5 +1,7 @@
    +from __future__ import annotations
    +
     import html
    -from typing import Optional, Union
    +from typing import Callable, Literal
     
     from .html import Html
     from .mixins.label_element import LabelElement
    @@ -8,35 +10,47 @@
     class ChatMessage(LabelElement):
     
         def __init__(self,
    -                 text: Union[str, list[str]] = ..., *,  # type: ignore
    -                 name: Optional[str] = None,
    -                 label: Optional[str] = None,
    -                 stamp: Optional[str] = None,
    -                 avatar: Optional[str] = None,
    +                 text: str | list[str] | None = None,
    +                 name: str | None = None,
    +                 label: str | None = None,
    +                 stamp: str | None = None,
    +                 avatar: str | None = None,
                      sent: bool = False,
                      text_html: bool = False,
    +                 sanitize: Callable[[str], str] | Literal[False] | None = None,
                      ) -> None:
             """Chat Message
     
             Based on Quasar's `Chat Message <https://quasar.dev/vue-components/chat/>`_ component.
     
    +        Note that since NiceGUI 3.0, you need to specify how to ``sanitize`` the HTML content
    +        if you activate HTML via ``text_html=True``.
    +        Especially if you are displaying user input, you should sanitize the content to prevent XSS attacks.
    +        We recommend ``Sanitizer().sanitize`` which requires the html-sanitizer package to be installed.
    +        If you are not displaying user input, you can pass ``False`` to disable sanitization.
    +
             :param text: the message body (can be a list of strings for multiple message parts)
             :param name: the name of the message author
             :param label: renders a label header/section only
             :param stamp: timestamp of the message
             :param avatar: URL to an avatar
    -        :param sent: render as a sent message (so from current user) (default: False)
    -        :param text_html: render text as HTML (default: False)
    +        :param sent: render as a sent message (so from current user) (default: ``False``)
    +        :param text_html: render text as HTML (consider using a ``sanitize`` function to prevent XSS attacks) (default: ``False``)
    +        :param sanitize: a sanitize function to be applied to HTML content or ``False`` to deactivate sanitization (*added in version 3.0.0*)
             """
             super().__init__(tag='q-chat-message', label=label)
     
    -        if text is ...:
    +        if text is None:
                 text = []
             if isinstance(text, str):
                 text = [text]
             if not text_html:
                 text = [html.escape(part) for part in text]
                 text = [part.replace('\n', '<br />') for part in text]
    +            sanitize = False
    +
    +        if sanitize is None:
    +            raise ValueError('You must specify a sanitize function or sanitize=False when using text_html=True')
     
             if name is not None:
                 self._props['name'] = name
    @@ -48,4 +62,4 @@ def __init__(self,
     
             with self:
                 for line in text:
    -                Html(line)
    +                Html(line, sanitize=sanitize)
    
  • nicegui/elements/html.py+17 1 modified
    @@ -1,17 +1,33 @@
    +from __future__ import annotations
    +
    +from typing import Callable, Literal
    +
     from .mixins.content_element import ContentElement
     
     
     class Html(ContentElement):
     
    -    def __init__(self, content: str = '', *, tag: str = 'div') -> None:
    +    def __init__(self, content: str = '', *, sanitize: Callable[[str], str] | Literal[False], tag: str = 'div') -> None:
             """HTML Element
     
             Renders arbitrary HTML onto the page, wrapped in the specified tag.
             `Tailwind <https://tailwindcss.com/>`_ can be used for styling.
             You can also use `ui.add_head_html` to add html code into the head of the document and `ui.add_body_html`
             to add it into the body.
     
    +        Note that since NiceGUI 3.0, you need to specify how to ``sanitize`` the HTML content.
    +        Especially if you are displaying user input, you should sanitize the content to prevent XSS attacks.
    +        We recommend ``Sanitizer().sanitize`` which requires the html-sanitizer package to be installed.
    +        If you are not displaying user input, you can pass ``False`` to disable sanitization.
    +
             :param content: the HTML code to be displayed
    +        :param sanitize: a sanitize function to be applied to the content or ``False`` to deactivate sanitization (*added in version 3.0.0*)
             :param tag: the HTML tag to wrap the content in (default: "div")
             """
    +        self._sanitize = sanitize
             super().__init__(tag=tag, content=content)
    +
    +    def _handle_content_change(self, content: str) -> None:
    +        if self._sanitize:
    +            content = self._sanitize(content)
    +        super()._handle_content_change(content)
    
  • nicegui/error.py+1 1 modified
    @@ -29,7 +29,7 @@ def error_content(status_code: int, exception: Union[str, Exception] = '') -> No
                 message += ': ' + str(exception)
     
         with column().style('width: 100%; padding: 5rem 0; align-items: center; gap: 0'):
    -        html(SAD_FACE_SVG).style('width: 8rem; padding: 1.25rem 0')
    +        html(SAD_FACE_SVG, sanitize=False).style('width: 8rem; padding: 1.25rem 0')
             label(str(status_code)).style('font-size: 3.75rem; line-height: 1; padding: 1.25rem 0')
             label(title).style('font-size: 1.25rem; line-height: 1.75rem; padding: 1.25rem 0')
             label(message).style('font-size: 1.125rem; line-height: 1.75rem; color: rgb(107 114 128)')
    
  • poetry.lock+224 1 modified
    @@ -259,6 +259,29 @@ files = [
         {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"},
     ]
     
    +[[package]]
    +name = "beautifulsoup4"
    +version = "4.14.2"
    +description = "Screen-scraping library"
    +optional = false
    +python-versions = ">=3.7.0"
    +groups = ["dev"]
    +files = [
    +    {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"},
    +    {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"},
    +]
    +
    +[package.dependencies]
    +soupsieve = ">1.2"
    +typing-extensions = ">=4.0.0"
    +
    +[package.extras]
    +cchardet = ["cchardet"]
    +chardet = ["chardet"]
    +charset-normalizer = ["charset-normalizer"]
    +html5lib = ["html5lib"]
    +lxml = ["lxml"]
    +
     [[package]]
     name = "bidict"
     version = "0.23.1"
    @@ -1106,6 +1129,23 @@ files = [
         {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
     ]
     
    +[[package]]
    +name = "html-sanitizer"
    +version = "2.6.0"
    +description = "HTML sanitizer"
    +optional = false
    +python-versions = ">=3.8"
    +groups = ["dev"]
    +files = [
    +    {file = "html_sanitizer-2.6.0-py3-none-any.whl", hash = "sha256:44c8ef924358a092f13d7c63f6385b05994e5a328b0f11d1db421d88980b7c3f"},
    +    {file = "html_sanitizer-2.6.0.tar.gz", hash = "sha256:e20bbc92df161d16c04aace70b2f69f482bd1e60d40aaa8b3209a2aeb94cd524"},
    +]
    +
    +[package.dependencies]
    +beautifulsoup4 = "*"
    +lxml = ">=5.2"
    +lxml-html-clean = ">=0.4"
    +
     [[package]]
     name = "httpcore"
     version = "1.0.9"
    @@ -1581,6 +1621,177 @@ files = [
         {file = "libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880"},
     ]
     
    +[[package]]
    +name = "lxml"
    +version = "6.0.2"
    +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
    +optional = false
    +python-versions = ">=3.8"
    +groups = ["dev"]
    +files = [
    +    {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"},
    +    {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"},
    +    {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"},
    +    {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"},
    +    {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"},
    +    {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"},
    +    {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"},
    +    {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"},
    +    {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"},
    +    {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"},
    +    {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"},
    +    {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"},
    +    {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"},
    +    {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"},
    +    {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"},
    +    {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"},
    +    {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"},
    +    {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"},
    +    {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"},
    +    {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"},
    +    {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"},
    +    {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"},
    +    {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"},
    +    {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"},
    +    {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"},
    +    {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"},
    +    {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"},
    +    {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"},
    +    {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"},
    +    {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"},
    +    {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"},
    +    {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"},
    +    {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"},
    +    {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"},
    +    {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"},
    +    {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"},
    +    {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"},
    +    {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"},
    +    {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"},
    +    {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"},
    +    {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"},
    +    {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"},
    +    {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"},
    +    {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"},
    +    {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"},
    +    {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"},
    +    {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"},
    +    {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"},
    +    {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"},
    +    {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"},
    +    {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"},
    +    {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"},
    +    {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"},
    +    {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"},
    +    {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"},
    +    {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"},
    +    {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"},
    +    {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"},
    +    {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"},
    +    {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"},
    +    {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"},
    +    {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"},
    +    {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"},
    +    {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"},
    +    {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"},
    +    {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"},
    +    {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"},
    +    {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"},
    +    {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"},
    +    {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"},
    +    {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"},
    +    {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"},
    +    {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"},
    +    {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"},
    +    {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"},
    +    {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"},
    +    {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"},
    +    {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"},
    +    {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"},
    +    {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"},
    +    {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"},
    +    {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"},
    +    {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"},
    +    {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"},
    +]
    +
    +[package.extras]
    +cssselect = ["cssselect (>=0.7)"]
    +html-clean = ["lxml_html_clean"]
    +html5 = ["html5lib"]
    +htmlsoup = ["BeautifulSoup4"]
    +
    +[[package]]
    +name = "lxml-html-clean"
    +version = "0.4.2"
    +description = "HTML cleaner from lxml project"
    +optional = false
    +python-versions = "*"
    +groups = ["dev"]
    +files = [
    +    {file = "lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505"},
    +    {file = "lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3"},
    +]
    +
    +[package.dependencies]
    +lxml = "*"
    +
     [[package]]
     name = "markdown2"
     version = "2.5.4"
    @@ -3996,6 +4207,18 @@ files = [
         {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
     ]
     
    +[[package]]
    +name = "soupsieve"
    +version = "2.8"
    +description = "A modern CSS selector implementation for Beautiful Soup."
    +optional = false
    +python-versions = ">=3.9"
    +groups = ["dev"]
    +files = [
    +    {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"},
    +    {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"},
    +]
    +
     [[package]]
     name = "starlette"
     version = "0.47.2"
    @@ -4786,4 +5009,4 @@ sass = ["libsass"]
     [metadata]
     lock-version = "2.1"
     python-versions = "^3.9"
    -content-hash = "9d6714406fab51728130438e2f87929c18e049904302c9da7edc32848de9f014"
    +content-hash = "d5b4d17b0eb04d21e1eee5ddc8802c5e54191d205e65af12269c212b5f7fdcdd"
    
  • pyproject.toml+1 0 modified
    @@ -74,6 +74,7 @@ pylint = ">=3.3.1"
     cssbeautifier = "^1.15.4"
     rcssmin = "^1.2.1"
     tinycss2 = "^1.4.0"
    +html-sanitizer = "^2.6.0"
     
     [tool.poetry.scripts]
     nicegui-pack = "nicegui.scripts.pack:main"
    
  • tests/test_chat_message.py+52 0 added
    @@ -0,0 +1,52 @@
    +import pytest
    +from html_sanitizer import Sanitizer
    +from selenium.webdriver.common.by import By
    +
    +from nicegui import ui
    +from nicegui.testing import Screen
    +
    +
    +def test_text_vs_html(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.chat_message('10&euro;')
    +        ui.chat_message('20&euro;', text_html=True, sanitize=False)
    +        ui.chat_message('30&euro;', text_html=True, sanitize=Sanitizer().sanitize)
    +        ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"40&euro;"})>', text_html=True,
    +                        sanitize=False)
    +        ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"50&euro;"})>', text_html=True, sanitize=str)
    +        ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"60&euro;"})>', text_html=True,
    +                        sanitize=lambda x: x.replace('&euro;', 'EUR'))
    +        ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"70&euro;"})>', text_html=True,
    +                        sanitize=Sanitizer().sanitize)
    +        with pytest.raises(ValueError):
    +            ui.chat_message('80&euro;', text_html=True)
    +
    +    screen.open('/')
    +    screen.should_contain('10&euro;')
    +    screen.should_contain('20€')
    +    screen.should_contain('30€')
    +    screen.should_contain('40€')
    +    screen.should_contain('50€')
    +    screen.should_contain('60EUR')
    +    screen.should_not_contain('70€')
    +    screen.should_not_contain('80€')
    +
    +
    +def test_newline(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.chat_message('Hello\nNiceGUI!')
    +
    +    screen.open('/')
    +    assert screen.find('Hello').find_element(By.TAG_NAME, 'br')
    +
    +
    +def test_slot(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        with ui.chat_message():
    +            ui.label('slot')
    +
    +    screen.open('/')
    +    screen.should_contain('slot')
    
  • tests/test_chat.py+0 42 removed
    @@ -1,42 +0,0 @@
    -from selenium.webdriver.common.by import By
    -
    -from nicegui import ui
    -from nicegui.testing import Screen
    -
    -
    -def test_no_html(screen: Screen):
    -    @ui.page('/')
    -    def page():
    -        ui.chat_message('<strong>HTML</strong>')
    -
    -    screen.open('/')
    -    screen.should_contain('<strong>HTML</strong>')
    -
    -
    -def test_html(screen: Screen):
    -    @ui.page('/')
    -    def page():
    -        ui.chat_message('<strong>HTML</strong>', text_html=True)
    -
    -    screen.open('/')
    -    screen.should_contain('HTML')
    -    screen.should_not_contain('<strong>HTML</strong>')
    -
    -
    -def test_newline(screen: Screen):
    -    @ui.page('/')
    -    def page():
    -        ui.chat_message('Hello\nNiceGUI!')
    -
    -    screen.open('/')
    -    assert screen.find('Hello').find_element(By.TAG_NAME, 'br')
    -
    -
    -def test_slot(screen: Screen):
    -    @ui.page('/')
    -    def page():
    -        with ui.chat_message():
    -            ui.label('slot')
    -
    -    screen.open('/')
    -    screen.should_contain('slot')
    
  • tests/test_events.py+1 1 modified
    @@ -220,7 +220,7 @@ def page():
                 <p data-id="A">Item A</p>
                 <p data-id="B">Item B</p>
                 <p data-id="C">Item C</p>
    -        ''').on('click', lambda e: ids.append(e.args), js_handler='(e) => emit(e.target.dataset.id)')
    +        ''', sanitize=False).on('click', lambda e: ids.append(e.args), js_handler='(e) => emit(e.target.dataset.id)')
     
         screen.open('/')
         screen.click('Item A')
    
  • tests/test_html.py+18 1 modified
    @@ -1,11 +1,13 @@
    +from html_sanitizer import Sanitizer
    +
     from nicegui import html, ui
     from nicegui.testing import Screen
     
     
     def test_html_element(screen: Screen):
         @ui.page('/')
         def page():
    -        ui.html('This is strong.', tag='strong')
    +        ui.html('This is strong.', sanitize=False, tag='strong')
     
         screen.open('/')
         assert screen.find_by_tag('strong').text == 'This is strong.'
    @@ -19,3 +21,18 @@ def page():
         screen.open('/')
         screen.click('Click me')
         screen.should_contain('Hi!')
    +
    +
    +def test_sanitize(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.html('<img src=x onerror=Quasar.Notify.create({message:"A"})>', sanitize=False)
    +        ui.html('<img src=x onerror=Quasar.Notify.create({message:"B"})>', sanitize=str)
    +        ui.html('<img src=x onerror=Quasar.Notify.create({message:"C"})>', sanitize=lambda x: x.replace('C', 'C!'))
    +        ui.html('<img src=x onerror=Quasar.Notify.create({message:"D"})>', sanitize=Sanitizer().sanitize)
    +
    +    screen.open('/')
    +    screen.should_contain('A')
    +    screen.should_contain('B')
    +    screen.should_contain('C!')
    +    screen.should_not_contain('D')
    
  • website/documentation/content/chat_message_documentation.py+6 3 modified
    @@ -13,10 +13,14 @@ def main_demo() -> None:
     
     @doc.demo('HTML text', '''
         Using the `text_html` parameter, you can send HTML text to the chat.
    +    Note that you should specify a sanitize function when displaying user input as HTML to prevent XSS attacks.
     ''')
     def html_text():
    +    from html_sanitizer import Sanitizer
    +
         ui.chat_message('Without <strong>HTML</strong>')
    -    ui.chat_message('With <strong>HTML</strong>', text_html=True)
    +    ui.chat_message('With <strong>HTML</strong>',
    +                    text_html=True, sanitize=Sanitizer().sanitize)
     
     
     @doc.demo('Newline', '''
    @@ -30,8 +34,7 @@ def newline():
         You can send multiple message parts by passing a list of strings.
     ''')
     def multiple_messages():
    -    ui.chat_message(['Hi! 😀', 'How are you?']
    -                    )
    +    ui.chat_message(['Hi! 😀', 'How are you?'])
     
     
     @doc.demo('Chat message with child elements', '''
    
  • website/documentation/content/element_filter_documentation.py+1 1 modified
    @@ -31,7 +31,7 @@ def text_element() -> None:
             ui.icon('home')
             ui.label('label A')
             ui.label('label B')
    -        ui.html('HTML')
    +        ui.html('HTML', sanitize=False)
     
         # ui.label(', '.join(b.text for b in ElementFilter(kind=TextElement)))
         # END OF DEMO
    
  • website/documentation/content/html_documentation.py+2 2 modified
    @@ -5,14 +5,14 @@
     
     @doc.demo(ui.html)
     def main_demo() -> None:
    -    ui.html('This is <strong>HTML</strong>.')
    +    ui.html('This is <strong>HTML</strong>.', sanitize=False)
     
     
     @doc.demo('Producing in-line elements', '''
         Use the `tag` parameter to produce something other than a div.
     ''')
     def demo_inline() -> None:
    -    ui.html('This is <u>emphasized</u>.', tag='em')
    +    ui.html('This is <u>emphasized</u>.', tag='em', sanitize=False)
     
     
     @doc.demo(other_html_elements_title := 'Other HTML Elements', other_html_elements_description := '''
    
  • website/documentation/content/icon_documentation.py+1 1 modified
    @@ -57,7 +57,7 @@ def lottie():
         ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
     
         src = 'https://assets5.lottiefiles.com/packages/lf20_MKCnqtNQvg.json'
    -    ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-24')
    +    ui.html(f'<lottie-player src="{src}" loop autoplay />', sanitize=False).classes('w-24')
     
     
     doc.reference(ui.icon)
    
  • website/documentation/content/image_documentation.py+1 1 modified
    @@ -41,7 +41,7 @@ def lottie():
         ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
     
         src = 'https://assets1.lottiefiles.com/datafiles/HN7OcWNnoqje6iXIiZdWzKxvLIbfeCGTmvXmEm1h/data.json'
    -    ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-full')
    +    ui.html(f'<lottie-player src="{src}" loop autoplay />', sanitize=False).classes('w-full')
     
     
     @doc.demo('Image link', '''
    
  • website/documentation/content/notify_documentation.py+1 1 modified
    @@ -22,7 +22,7 @@ def notify_colors():
         If manual newline breaks are required (e.g. `\\n`), you need to define a CSS style and pass it to the notification as shown in the example.
     ''')
     def multiline():
    -    ui.html('<style>.multi-line-notification { white-space: pre-line; }</style>')
    +    ui.html('<style>.multi-line-notification { white-space: pre-line; }</style>', sanitize=False)
         ui.button('show', on_click=lambda: ui.notify(
             'Lorem ipsum dolor sit amet, consectetur adipisicing elit. \n'
             'Hic quisquam non ad sit assumenda consequuntur esse inventore officia. \n'
    
  • website/documentation/content/overview.py+1 1 modified
    @@ -143,7 +143,7 @@
     def create_tiles():
         with ui.row().classes('items-center content-between'):
             ui.label('If you like NiceGUI, go and become a')
    -        ui.html('<iframe src="https://github.com/sponsors/zauberzeug/button" title="Sponsor zauberzeug" height="32" width="114" style="border: 0; border-radius: 6px;"></iframe>')
    +        ui.html('<iframe src="https://github.com/sponsors/zauberzeug/button" title="Sponsor zauberzeug" height="32" width="114" style="border: 0; border-radius: 6px;"></iframe>', sanitize=False)
         for documentation, description in tiles:
             page = doc.get_page(documentation)
             with ui.link(target=f'/documentation/{page.name}') \
    
  • website/documentation/content/run_documentation.py+1 1 modified
    @@ -50,7 +50,7 @@ def base64_favicon():
                 <circle cx="120" cy="85" r="8" />
                 <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
             </svg>
    -    ''').classes('w-4 h-4'),
    +    ''', sanitize=False).classes('w-4 h-4'),
         ui.label('NiceGUI'),
     ))
     def svg_favicon():
    
  • website/documentation/content/section_audiovisual_elements.py+2 2 modified
    @@ -30,7 +30,7 @@ def captions_and_overlays_demo():
                 <svg viewBox="0 0 960 638" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
                 <circle cx="445" cy="300" r="100" fill="none" stroke="red" stroke-width="10" />
                 </svg>
    -        ''').classes('w-full bg-transparent')
    +        ''', sanitize=False).classes('w-full bg-transparent')
     
     
     doc.intro(interactive_image_documentation)
    @@ -51,4 +51,4 @@ def svg_demo():
             <circle cx="120" cy="85" r="8" />
             <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
             </svg>'''
    -    ui.html(content)
    +    ui.html(content, sanitize=False)
    
  • website/documentation/content/section_styling_appearance.py+2 2 modified
    @@ -182,7 +182,7 @@ def overwrite_tailwind_style_demo():
                 }
             </style>
         ''')
    -    ui.html('<h2>Hello world!</h2>')
    +    ui.html('<h2>Hello world!</h2>', sanitize=False)
     
     
     doc.intro(dark_mode_documentation)
    @@ -213,7 +213,7 @@ def other_vue_ui_frameworks_demo():
         # '''
     
         with ui.element('el-button').on('click', lambda: ui.notify('Hi!')):
    -        ui.html('Element Plus button')
    +        ui.html('Element Plus button', sanitize=False)
     
         ui.button('Quasar button', on_click=lambda: ui.notify('Ho!'))
     
    
  • website/documentation/content/tooltip_documentation.py+2 2 modified
    @@ -25,7 +25,7 @@ def tooltip_method_demo():
     def tooltip_html_demo():
         with ui.label('HTML...'):
             with ui.tooltip():
    -            ui.html('<b>b</b>, <em>em</em>, <u>u</u>, <s>s</s>')
    +            ui.html('<b>b</b>, <em>em</em>, <u>u</u>, <s>s</s>', sanitize=False)
     
     
     @doc.demo('Tooltip with other content', '''
    @@ -43,7 +43,7 @@ def tooltip_with_other_content():
     ''')
     def tooltip_on_html_and_markdown():
         with ui.element().tooltip('...with a tooltip!'):
    -        ui.html('This is <u>HTML</u>...')
    +        ui.html('This is <u>HTML</u>...', sanitize=False)
     
     
     @doc.demo('Tooltip for the upload element', '''
    
  • website/documentation/intro.py+1 1 modified
    @@ -13,7 +13,7 @@ def create_intro() -> None:
         def formatting_demo():
             ui.icon('thumb_up')
             ui.markdown('This is **Markdown**.')
    -        ui.html('This is <strong>HTML</strong>.')
    +        ui.html('This is <strong>HTML</strong>.', sanitize=False)
             with ui.row():
                 ui.label('CSS').style('color: #888; font-weight: bold')
                 ui.label('Tailwind').classes('font-serif')
    
  • website/main_page.py+4 4 modified
    @@ -52,7 +52,7 @@ def create() -> None:
             section_heading('Installation', 'Get *started*')
             with ui.row().classes('w-full text-lg leading-tight grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8'):
                 with ui.column().classes('w-full max-w-md gap-2'):
    -                ui.html('<em>1.</em>').classes('text-3xl font-bold fancy-em')
    +                ui.html('<em>1.</em>', sanitize=False).classes('text-3xl font-bold fancy-em')
                     ui.markdown('Create __main.py__').classes('text-lg')
                     with documentation.python_window(classes='w-full h-52'):
                         ui.markdown('''
    @@ -65,7 +65,7 @@ def create() -> None:
                             ```
                         ''')
                 with ui.column().classes('w-full max-w-md gap-2'):
    -                ui.html('<em>2.</em>').classes('text-3xl font-bold fancy-em')
    +                ui.html('<em>2.</em>', sanitize=False).classes('text-3xl font-bold fancy-em')
                     ui.markdown('Install and launch').classes('text-lg')
                     with documentation.bash_window(classes='w-full h-52'):
                         ui.markdown('''
    @@ -75,7 +75,7 @@ def create() -> None:
                             ```
                         ''')
                 with ui.column().classes('w-full max-w-md gap-2'):
    -                ui.html('<em>3.</em>').classes('text-3xl font-bold fancy-em')
    +                ui.html('<em>3.</em>', sanitize=False).classes('text-3xl font-bold fancy-em')
                     ui.markdown('Enjoy!').classes('text-lg')
                     with documentation.browser_window(classes='w-full h-52'):
                         ui.label('Hello NiceGUI!')
    @@ -147,7 +147,7 @@ def create() -> None:
                 with ui.column().classes('gap-1 max-lg:items-center max-lg:text-center'):
                     ui.markdown('Browse through plenty of live demos.') \
                         .classes('text-white text-2xl md:text-3xl font-medium')
    -                ui.html('Fun-Fact: This whole website is also coded with NiceGUI.') \
    +                ui.label('Fun-Fact: This whole website is also coded with NiceGUI.') \
                         .classes('text-white text-lg md:text-xl')
                 ui.link('Documentation', '/documentation').style('color: black !important') \
                     .classes('rounded-full mx-auto px-12 py-2 bg-white font-medium text-lg md:text-xl')
    
  • website/style.py+1 1 modified
    @@ -64,7 +64,7 @@ def side_menu() -> ui.left_drawer:
     def subheading(text: str, *, link: Optional[str] = None, major: bool = False, anchor_name: Optional[str] = None) -> None:
         """Render a subheading with an anchor that can be linked to with a hash."""
         name = anchor_name or create_anchor_name(text)
    -    ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
    +    ui.html(f'<div id="{name}"></div>', sanitize=False).style('position: relative; top: -90px')
         with ui.row().classes('gap-2 items-center relative'):
             classes = 'text-3xl' if major else 'text-2xl'
             if link:
    
  • website/svg.py+5 5 modified
    @@ -14,20 +14,20 @@ def face(half: bool = False) -> ui.html:
         code = HAPPY_FACE_SVG
         if half:
             code = code.replace('viewBox="0 0 62.44 71.74"', 'viewBox="31.22 0 31.22 71.74"')
    -    return ui.html(code)
    +    return ui.html(code, sanitize=False)
     
     
     def word() -> ui.html:
    -    return ui.html(NICEGUI_WORD_SVG)
    +    return ui.html(NICEGUI_WORD_SVG, sanitize=False)
     
     
     def github() -> ui.html:
    -    return ui.html(GITHUB_SVG)
    +    return ui.html(GITHUB_SVG, sanitize=False)
     
     
     def discord() -> ui.html:
    -    return ui.html(DISCORD_SVG)
    +    return ui.html(DISCORD_SVG, sanitize=False)
     
     
     def reddit() -> ui.html:
    -    return ui.html(REDDIT_SVG)
    +    return ui.html(REDDIT_SVG, sanitize=False)
    

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

News mentions

0

No linked articles in our index yet.