VYPR
Moderate severityNVD Advisory· Published Feb 6, 2026· Updated Feb 9, 2026

NiceGUI's XSS vulnerability in ui.markdown() allows arbitrary JavaScript execution through unsanitized HTML content

CVE-2026-25516

Description

NiceGUI is a Python-based UI framework. 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. This vulnerability is fixed in 3.7.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
niceguiPyPI
< 3.7.03.7.0

Affected products

1

Patches

1
f1f753357787

Merge commit from fork

https://github.com/zauberzeug/niceguiFalko SchindlerFeb 5, 2026via ghsa
19 files changed · +189 47
  • extract_core_libraries.py+2 0 modified
    @@ -84,3 +84,5 @@ def _minify_js(input_path: Path, output_path: Path) -> None:
                  STATIC / 'unocss' / 'preset-wind3.global.js')
     shutil.copy2(NODE_MODULES / '@unocss' / 'runtime' / 'preset-wind4.global.js',
                  STATIC / 'unocss' / 'preset-wind4.global.js')
    +
    +_minify_js(NODE_MODULES / 'dompurify' / 'dist' / 'purify.es.mjs', STATIC / 'dompurify.mjs')
    
  • .gitattributes+1 0 modified
    @@ -1,5 +1,6 @@
     nicegui/elements/*/dist/**         linguist-vendored
     nicegui/elements/anywidget/lib/**  linguist-vendored
    +nicegui/static/dompurify.*         linguist-vendored
     nicegui/static/fonts/**            linguist-vendored
     nicegui/static/fonts.css           linguist-vendored
     nicegui/static/immutable.*         linguist-vendored
    
  • nicegui/dependencies.py+1 0 modified
    @@ -196,6 +196,7 @@ def generate_resources(prefix: str, elements: Iterable[Element]) -> tuple[list[s
             'vue': f'{prefix}/_nicegui/{__version__}/static/vue.esm-browser{".prod" if core.app.config.prod_js else ""}.js',
             'sass': f'{prefix}/_nicegui/{__version__}/static/sass.default.js',
             'immutable': f'{prefix}/_nicegui/{__version__}/static/immutable.es.js',
    +        'dompurify': f'{prefix}/_nicegui/{__version__}/static/dompurify.mjs',
         }
         js_imports: list[str] = []
         js_imports_urls: list[str] = [imports['vue']]
    
  • nicegui/elements/chat_message.py+11 13 modified
    @@ -1,7 +1,7 @@
     import html
     from collections.abc import Callable
    -from typing import Literal
     
    +from .. import helpers
     from ..defaults import DEFAULT_PROP, resolve_defaults
     from .html import Html
     from .mixins.label_element import LabelElement
    @@ -18,29 +18,30 @@ def __init__(self,
                      avatar: str | None = DEFAULT_PROP | None,
                      sent: bool = DEFAULT_PROP | False,
                      text_html: bool = False,
    -                 sanitize: Callable[[str], str] | Literal[False] | None = None,
    +                 sanitize: Callable[[str], str] | bool | None = True,  # DEPRECATED: remove `None` in version 4.0.0
                      ) -> 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 (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*)
    +        :param text_html: render text as HTML (default: ``False``)
    +        :param sanitize: sanitization mode (only relevant when ``text_html=True``):
    +            ``True`` (default) uses client-side sanitization via setHTML or DOMPurify,
    +            ``False`` disables sanitization (use only with trusted content),
    +            or pass a callable to apply server-side sanitization
             """
             super().__init__(tag='q-chat-message', label=label)
     
    +        if sanitize is None:
    +            helpers.warn_once('`sanitize=None` is deprecated, defaults to `True` and will be removed in version 4.0.0.')
    +            sanitize = True  # DEPRECATED: remove this block in version 4.0.0
    +
             if text is None:
                 text = []
             if isinstance(text, str):
    @@ -50,9 +51,6 @@ def __init__(self,
                 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')
    -
             self._props.set_optional('name', name)
             self._props.set_optional('stamp', stamp)
             self._props.set_optional('avatar', avatar)
    
  • nicegui/elements/html.js+23 0 added
    @@ -0,0 +1,23 @@
    +export default {
    +  template: `<component :is="tag"></component>`,
    +  mounted() {
    +    this.renderContent();
    +  },
    +  updated() {
    +    this.renderContent();
    +  },
    +  methods: {
    +    renderContent() {
    +      if (this.sanitize) {
    +        this.$el.setHTML(this.innerHTML);
    +      } else {
    +        this.$el.innerHTML = this.innerHTML;
    +      }
    +    },
    +  },
    +  props: {
    +    innerHTML: String,
    +    sanitize: Boolean,
    +    tag: String,
    +  },
    +};
    
  • nicegui/elements/html.py+10 11 modified
    @@ -1,32 +1,31 @@
     from collections.abc import Callable
    -from typing import Literal
     
     from .mixins.content_element import ContentElement
     
     
    -class Html(ContentElement):
    +class Html(ContentElement, component='html.js'):
     
    -    def __init__(self, content: str = '', *, sanitize: Callable[[str], str] | Literal[False], tag: str = 'div') -> None:
    +    def __init__(self, content: str = '', *, sanitize: Callable[[str], str] | bool = True, 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 sanitize: sanitization mode:
    +            ``True`` (default) uses client-side sanitization via setHTML or DOMPurify,
    +            ``False`` disables sanitization (use only with trusted content),
    +            or pass a callable to apply server-side sanitization
             :param tag: the HTML tag to wrap the content in (default: "div")
             """
             self._sanitize = sanitize
    -        super().__init__(tag=tag, content=content)
    +        super().__init__(content=content)
    +        self._props['tag'] = tag
    +        self._props['sanitize'] = sanitize is True
     
         def _handle_content_change(self, content: str) -> None:
    -        if self._sanitize:
    +        if callable(self._sanitize):
                 content = self._sanitize(content)
             super()._handle_content_change(content)
    
  • nicegui/elements/interactive_image.js+25 1 modified
    @@ -16,7 +16,7 @@ export default {
               <line v-if="cross" x1="0" :y1="y" x2="100%" :y2="y" :stroke="cross === true ? 'black' : cross" />
               <slot name="cross" :x="x" :y="y"></slot>
             </g>
    -        <g v-html="content"></g>
    +        <g ref="contentGroup"></g>
           </svg>
           <slot></slot>
         </div>
    @@ -32,9 +32,18 @@ export default {
           computed_src: undefined,
           waiting_source: undefined,
           loading: false,
    +      DOMPurify: null,
         };
       },
       mounted() {
    +    if (this.sanitize) {
    +      import("dompurify").then(({ default: DOMPurify }) => {
    +        this.DOMPurify = DOMPurify;
    +        this.renderContent();
    +      });
    +    } else {
    +      this.renderContent();
    +    }
         setTimeout(() => this.compute_src(), 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
         const handle_completion = () => {
           if (this.waiting_source) {
    @@ -60,9 +69,23 @@ export default {
         }
       },
       updated() {
    +    this.renderContent();
         this.compute_src();
       },
       methods: {
    +    renderContent() {
    +      const content = this.content || "";
    +      if (this.sanitize) {
    +        if (!this.DOMPurify) return;
    +        const sanitized = this.DOMPurify.sanitize(`<svg>${content}</svg>`, {
    +          USE_PROFILES: { svg: true, svgFilters: true },
    +        });
    +        const match = sanitized.match(/^<svg>(.*)<\/svg>$/is);
    +        this.$refs.contentGroup.innerHTML = match ? match[1] : "";
    +      } else {
    +        this.$refs.contentGroup.innerHTML = content;
    +      }
    +    },
         compute_src() {
           const suffix = this.t ? (this.src.includes("?") ? "&" : "?") + "_nicegui_t=" + this.t : "";
           const new_src = (this.src.startsWith("/") ? window.path_prefix : "") + this.src + suffix;
    @@ -144,5 +167,6 @@ export default {
         events: Array,
         cross: Boolean,
         t: String,
    +    sanitize: Boolean,
       },
     };
    
  • nicegui/elements/interactive_image.py+22 17 modified
    @@ -4,7 +4,7 @@
     from collections.abc import Callable
     from contextlib import suppress
     from pathlib import Path
    -from typing import Literal, cast
    +from typing import cast
     
     from typing_extensions import Self
     
    @@ -33,7 +33,7 @@ def __init__(self,
                      on_mouse: Handler[MouseEventArguments] | None = None,
                      events: list[str] = DEFAULT_PROP | ['click'],
                      cross: bool | str = DEFAULT_PROP | False,
    -                 sanitize: Callable[[str], str] | Literal[False] | None = None,
    +                 sanitize: Callable[[str], str] | bool | None = True,  # DEPRECATED: remove `None` in version 4.0.0
                      ) -> None:
             """Interactive Image
     
    @@ -53,24 +53,27 @@ def __init__(self,
             You can also pass a tuple of width and height instead of an image source.
             This will create an empty image with the given size.
     
    -        Note that since NiceGUI 3.4.0, you need to specify how to ``sanitize`` the SVG content (if any).
    -        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 source: the source of the image; can be an URL, local file path, a base64 string or just an image size
             :param content: SVG content which should be overlaid; viewport has the same dimensions as the image
             :param size: size of the image (width, height) in pixels; only used if `source` is not set
             :param on_mouse: callback for mouse events (contains image coordinates `image_x` and `image_y` in pixels)
             :param events: list of JavaScript events to subscribe to (default: `['click']`)
             :param cross: whether to show crosshairs or a color string (default: `False`)
    -        :param sanitize: a sanitize function to be applied to the content, ``False`` to deactivate sanitization (default ``None``: warns if content is provided, *added in version 3.4.0*)
    +        :param sanitize: sanitization mode:
    +            ``True`` (default) uses client-side sanitization via DOMPurify,
    +            ``False`` disables sanitization (use only with trusted content),
    +            or pass a callable to apply server-side sanitization
             """
    +        if sanitize is None:
    +            helpers.warn_once('`sanitize=None` is deprecated, defaults to `True` and will be removed in version 4.0.0.')
    +            sanitize = True
    +
             self._sanitize = sanitize
             super().__init__(source=source, content=content)
             self._props['events'] = events[:]
             self._props['cross'] = cross
             self._props['size'] = size
    +        self._props['sanitize'] = sanitize is True
     
             if on_mouse:
                 self.on_mouse(on_mouse)
    @@ -127,10 +130,9 @@ def add_layer(self, *, content: str = '') -> InteractiveImageLayer:
                 return layer
     
         def _handle_content_change(self, content: str) -> None:
    -        if content and self._sanitize is None:
    -            helpers.warn_once('ui.interactive_image: content provided but no explicit sanitize function set; '
    -                              'to avoid XSS vulnerabilities, please provide a sanitize function or set sanitize=False')
    -        return super()._handle_content_change(self._sanitize(content) if self._sanitize else content)
    +        if callable(self._sanitize):
    +            content = self._sanitize(content)
    +        return super()._handle_content_change(content)
     
     
     class InteractiveImageLayer(SourceElement, ContentElement, component='interactive_image.js'):
    @@ -141,25 +143,28 @@ def __init__(self, *,
                      source: str,
                      content: str,
                      size: tuple[float, float] | None,
    -                 sanitize: Callable[[str], str] | Literal[False] | None = None,
    +                 sanitize: Callable[[str], str] | bool | None = True,  # DEPRECATED: remove `None` in version 4.0.0
                      ) -> None:
             """Interactive Image Layer
     
             This element is created when adding a layer to an ``InteractiveImage``.
     
             *Added in version 2.17.0*
             """
    +        if sanitize is None:
    +            helpers.warn_once('`sanitize=None` is deprecated, defaults to `True` and will be removed in version 4.0.0.')
    +            sanitize = True
             self._sanitize = sanitize
             super().__init__(source=source, content=content)
             self._props['size'] = size
    +        self._props['sanitize'] = sanitize is True
     
         def _set_props(self, source: str | Path | PIL_Image) -> None:
             if optional_features.has('pillow') and isinstance(source, PIL_Image):
                 source = pil_to_tempfile(source, self.PIL_CONVERT_FORMAT)
             super()._set_props(source)
     
         def _handle_content_change(self, content: str) -> None:
    -        if self._sanitize is None:
    -            helpers.warn_once('ui.interactive_image layer: content provided but no explicit sanitize function set; '
    -                              'to avoid XSS vulnerabilities, please provide a sanitize function or set sanitize=False')
    -        return super()._handle_content_change(self._sanitize(content) if self._sanitize else content)
    +        if callable(self._sanitize):
    +            content = self._sanitize(content)
    +        return super()._handle_content_change(content)
    
  • nicegui/elements/markdown.js+11 0 modified
    @@ -5,6 +5,7 @@ export default {
       async mounted() {
         await this.$nextTick(); // NOTE: wait for window.path_prefix to be set
         await loadResource(window.path_prefix + `${this.dynamicResourcePath}/${this.resourceName}`);
    +    this.renderContent();
         if (this.useMermaid) {
           this.mermaid = (await import("nicegui-mermaid")).mermaid;
           this.mermaid.initialize({ startOnLoad: false });
    @@ -18,9 +19,17 @@ export default {
         };
       },
       updated() {
    +    this.renderContent();
         this.renderMermaid();
       },
       methods: {
    +    renderContent() {
    +      if (this.sanitize) {
    +        this.$el.setHTML(this.innerHTML);
    +      } else {
    +        this.$el.innerHTML = this.innerHTML;
    +      }
    +    },
         renderMermaid() {
           if (!this.useMermaid || !this.mermaid) return;
           // render new diagrams
    @@ -53,8 +62,10 @@ export default {
         },
       },
       props: {
    +    innerHTML: String,
         dynamicResourcePath: String,
         resourceName: String,
    +    sanitize: Boolean,
         useMermaid: {
           required: false,
           default: false,
    
  • nicegui/elements/markdown.py+10 0 modified
    @@ -1,5 +1,6 @@
     import hashlib
     import os
    +from collections.abc import Callable
     from functools import lru_cache
     
     import markdown2
    @@ -16,16 +17,23 @@ class Markdown(ContentElement, component='markdown.js', default_classes='nicegui
         def __init__(self,
                      content: str = '', *,
                      extras: list[str] = ['fenced-code-blocks', 'tables'],  # noqa: B006
    +                 sanitize: Callable[[str], str] | bool = True,
                      ) -> None:
             """Markdown Element
     
             Renders Markdown onto the page.
     
             :param content: the Markdown content to be displayed
             :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
    +        :param sanitize: sanitization mode:
    +            ``True`` (default) uses client-side sanitization via setHTML or DOMPurify,
    +            ``False`` disables sanitization (use only with trusted content),
    +            or pass a callable to apply server-side sanitization
             """
    +        self._sanitize = sanitize
             self.extras = extras[:]
             super().__init__(content=content)
    +        self._props['sanitize'] = sanitize is True
             if 'mermaid' in extras:
                 self._props['use-mermaid'] = True
     
    @@ -53,6 +61,8 @@ def _generate_codehilite_css() -> str:
     
         def _handle_content_change(self, content: str) -> None:
             html = prepare_content(content, extras=' '.join(self.extras))
    +        if callable(self._sanitize):
    +            html = self._sanitize(html)
             if self._props.get('innerHTML') != html:
                 self._props['innerHTML'] = html
     
    
  • nicegui/static/dompurify.mjs+2 0 added
    @@ -0,0 +1,2 @@
    +/*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */
    +const{entries:entries,setPrototypeOf:setPrototypeOf,isFrozen:isFrozen,getPrototypeOf:getPrototypeOf,getOwnPropertyDescriptor:getOwnPropertyDescriptor}=Object;let{freeze:freeze,seal:seal,create:create}=Object,{apply:apply,construct:construct}="undefined"!=typeof Reflect&&Reflect;freeze||(freeze=function(e){return e}),seal||(seal=function(e){return e}),apply||(apply=function(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),o=2;o<n;o++)r[o-2]=arguments[o];return e.apply(t,r)}),construct||(construct=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];return new e(...n)});const arrayForEach=unapply(Array.prototype.forEach),arrayLastIndexOf=unapply(Array.prototype.lastIndexOf),arrayPop=unapply(Array.prototype.pop),arrayPush=unapply(Array.prototype.push),arraySplice=unapply(Array.prototype.splice),stringToLowerCase=unapply(String.prototype.toLowerCase),stringToString=unapply(String.prototype.toString),stringMatch=unapply(String.prototype.match),stringReplace=unapply(String.prototype.replace),stringIndexOf=unapply(String.prototype.indexOf),stringTrim=unapply(String.prototype.trim),objectHasOwnProperty=unapply(Object.prototype.hasOwnProperty),regExpTest=unapply(RegExp.prototype.test),typeErrorCreate=unconstruct(TypeError);function unapply(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return apply(e,t,r)}}function unconstruct(e){return function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return construct(e,n)}}function addToSet(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:stringToLowerCase;setPrototypeOf&&setPrototypeOf(e,null);let r=t.length;for(;r--;){let o=t[r];if("string"==typeof o){const e=n(o);e!==o&&(isFrozen(t)||(t[r]=e),o=e)}e[o]=!0}return e}function cleanArray(e){for(let t=0;t<e.length;t++){objectHasOwnProperty(e,t)||(e[t]=null)}return e}function clone(e){const t=create(null);for(const[n,r]of entries(e)){objectHasOwnProperty(e,n)&&(Array.isArray(r)?t[n]=cleanArray(r):r&&"object"==typeof r&&r.constructor===Object?t[n]=clone(r):t[n]=r)}return t}function lookupGetter(e,t){for(;null!==e;){const n=getOwnPropertyDescriptor(e,t);if(n){if(n.get)return unapply(n.get);if("function"==typeof n.value)return unapply(n.value)}e=getPrototypeOf(e)}return function(){return null}}const html$1=freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),svg$1=freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),svgFilters=freeze(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),svgDisallowed=freeze(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),mathMl$1=freeze(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),mathMlDisallowed=freeze(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),text=freeze(["#text"]),html=freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),svg=freeze(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),mathMl=freeze(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),xml=freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),MUSTACHE_EXPR=seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm),ERB_EXPR=seal(/<%[\w\W]*|[\w\W]*%>/gm),TMPLIT_EXPR=seal(/\$\{[\w\W]*/gm),DATA_ATTR=seal(/^data-[\-\w.\u00B7-\uFFFF]+$/),ARIA_ATTR=seal(/^aria-[\-\w]+$/),IS_ALLOWED_URI=seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),IS_SCRIPT_OR_DATA=seal(/^(?:\w+script|data):/i),ATTR_WHITESPACE=seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),DOCTYPE_NAME=seal(/^html$/i),CUSTOM_ELEMENT=seal(/^[a-z][.\w]*(-[.\w]+)+$/i);var EXPRESSIONS=Object.freeze({__proto__:null,ARIA_ATTR:ARIA_ATTR,ATTR_WHITESPACE:ATTR_WHITESPACE,CUSTOM_ELEMENT:CUSTOM_ELEMENT,DATA_ATTR:DATA_ATTR,DOCTYPE_NAME:DOCTYPE_NAME,ERB_EXPR:ERB_EXPR,IS_ALLOWED_URI:IS_ALLOWED_URI,IS_SCRIPT_OR_DATA:IS_SCRIPT_OR_DATA,MUSTACHE_EXPR:MUSTACHE_EXPR,TMPLIT_EXPR:TMPLIT_EXPR});const NODE_TYPE={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},getGlobal=function(){return"undefined"==typeof window?null:window},_createTrustedTypesPolicy=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const r="data-tt-policy-suffix";t&&t.hasAttribute(r)&&(n=t.getAttribute(r));const o="dompurify"+(n?"#"+n:"");try{return e.createPolicy(o,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+o+" could not be created."),null}},_createHooksMap=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function createDOMPurify(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:getGlobal();const t=e=>createDOMPurify(e);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==NODE_TYPE.document||!e.Element)return t.isSupported=!1,t;let{document:n}=e;const r=n,o=r.currentScript,{DocumentFragment:a,HTMLTemplateElement:i,Node:l,Element:s,NodeFilter:c,NamedNodeMap:p=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:u,DOMParser:m,trustedTypes:d}=e,f=s.prototype,T=lookupGetter(f,"cloneNode"),g=lookupGetter(f,"remove"),h=lookupGetter(f,"nextSibling"),y=lookupGetter(f,"childNodes"),E=lookupGetter(f,"parentNode");if("function"==typeof i){const e=n.createElement("template");e.content&&e.content.ownerDocument&&(n=e.content.ownerDocument)}let S,_="";const{implementation:A,createNodeIterator:b,createDocumentFragment:N,getElementsByTagName:O}=n,{importNode:R}=r;let D=_createHooksMap();t.isSupported="function"==typeof entries&&"function"==typeof E&&A&&void 0!==A.createHTMLDocument;const{MUSTACHE_EXPR:w,ERB_EXPR:C,TMPLIT_EXPR:x,DATA_ATTR:v,ARIA_ATTR:I,IS_SCRIPT_OR_DATA:L,ATTR_WHITESPACE:M,CUSTOM_ELEMENT:P}=EXPRESSIONS;let{IS_ALLOWED_URI:k}=EXPRESSIONS,z=null;const H=addToSet({},[...html$1,...svg$1,...svgFilters,...mathMl$1,...text]);let U=null;const F=addToSet({},[...html,...svg,...mathMl,...xml]);let G=Object.seal(create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),W=null,B=null;const j=Object.seal(create(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Y=!0,X=!0,$=!1,q=!0,K=!1,V=!0,Z=!1,J=!1,Q=!1,ee=!1,te=!1,ne=!1,re=!0,oe=!1;let ae=!0,ie=!1,le={},se=null;const ce=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let pe=null;const ue=addToSet({},["audio","video","img","source","image","track"]);let me=null;const de=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),fe="http://www.w3.org/1998/Math/MathML",Te="http://www.w3.org/2000/svg",ge="http://www.w3.org/1999/xhtml";let he=ge,ye=!1,Ee=null;const Se=addToSet({},[fe,Te,ge],stringToString);let _e=addToSet({},["mi","mo","mn","ms","mtext"]),Ae=addToSet({},["annotation-xml"]);const be=addToSet({},["title","style","font","a","script"]);let Ne=null;const Oe=["application/xhtml+xml","text/html"];let Re=null,De=null;const we=n.createElement("form"),Ce=function(e){return e instanceof RegExp||e instanceof Function},xe=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!De||De!==e){if(e&&"object"==typeof e||(e={}),e=clone(e),Ne=-1===Oe.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Re="application/xhtml+xml"===Ne?stringToString:stringToLowerCase,z=objectHasOwnProperty(e,"ALLOWED_TAGS")?addToSet({},e.ALLOWED_TAGS,Re):H,U=objectHasOwnProperty(e,"ALLOWED_ATTR")?addToSet({},e.ALLOWED_ATTR,Re):F,Ee=objectHasOwnProperty(e,"ALLOWED_NAMESPACES")?addToSet({},e.ALLOWED_NAMESPACES,stringToString):Se,me=objectHasOwnProperty(e,"ADD_URI_SAFE_ATTR")?addToSet(clone(de),e.ADD_URI_SAFE_ATTR,Re):de,pe=objectHasOwnProperty(e,"ADD_DATA_URI_TAGS")?addToSet(clone(ue),e.ADD_DATA_URI_TAGS,Re):ue,se=objectHasOwnProperty(e,"FORBID_CONTENTS")?addToSet({},e.FORBID_CONTENTS,Re):ce,W=objectHasOwnProperty(e,"FORBID_TAGS")?addToSet({},e.FORBID_TAGS,Re):clone({}),B=objectHasOwnProperty(e,"FORBID_ATTR")?addToSet({},e.FORBID_ATTR,Re):clone({}),le=!!objectHasOwnProperty(e,"USE_PROFILES")&&e.USE_PROFILES,Y=!1!==e.ALLOW_ARIA_ATTR,X=!1!==e.ALLOW_DATA_ATTR,$=e.ALLOW_UNKNOWN_PROTOCOLS||!1,q=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,K=e.SAFE_FOR_TEMPLATES||!1,V=!1!==e.SAFE_FOR_XML,Z=e.WHOLE_DOCUMENT||!1,ee=e.RETURN_DOM||!1,te=e.RETURN_DOM_FRAGMENT||!1,ne=e.RETURN_TRUSTED_TYPE||!1,Q=e.FORCE_BODY||!1,re=!1!==e.SANITIZE_DOM,oe=e.SANITIZE_NAMED_PROPS||!1,ae=!1!==e.KEEP_CONTENT,ie=e.IN_PLACE||!1,k=e.ALLOWED_URI_REGEXP||IS_ALLOWED_URI,he=e.NAMESPACE||ge,_e=e.MATHML_TEXT_INTEGRATION_POINTS||_e,Ae=e.HTML_INTEGRATION_POINTS||Ae,G=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&Ce(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(G.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&Ce(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(G.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(G.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),K&&(X=!1),te&&(ee=!0),le&&(z=addToSet({},text),U=[],!0===le.html&&(addToSet(z,html$1),addToSet(U,html)),!0===le.svg&&(addToSet(z,svg$1),addToSet(U,svg),addToSet(U,xml)),!0===le.svgFilters&&(addToSet(z,svgFilters),addToSet(U,svg),addToSet(U,xml)),!0===le.mathMl&&(addToSet(z,mathMl$1),addToSet(U,mathMl),addToSet(U,xml))),e.ADD_TAGS&&("function"==typeof e.ADD_TAGS?j.tagCheck=e.ADD_TAGS:(z===H&&(z=clone(z)),addToSet(z,e.ADD_TAGS,Re))),e.ADD_ATTR&&("function"==typeof e.ADD_ATTR?j.attributeCheck=e.ADD_ATTR:(U===F&&(U=clone(U)),addToSet(U,e.ADD_ATTR,Re))),e.ADD_URI_SAFE_ATTR&&addToSet(me,e.ADD_URI_SAFE_ATTR,Re),e.FORBID_CONTENTS&&(se===ce&&(se=clone(se)),addToSet(se,e.FORBID_CONTENTS,Re)),e.ADD_FORBID_CONTENTS&&(se===ce&&(se=clone(se)),addToSet(se,e.ADD_FORBID_CONTENTS,Re)),ae&&(z["#text"]=!0),Z&&addToSet(z,["html","head","body"]),z.table&&(addToSet(z,["tbody"]),delete W.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');S=e.TRUSTED_TYPES_POLICY,_=S.createHTML("")}else void 0===S&&(S=_createTrustedTypesPolicy(d,o)),null!==S&&"string"==typeof _&&(_=S.createHTML(""));freeze&&freeze(e),De=e}},ve=addToSet({},[...svg$1,...svgFilters,...svgDisallowed]),Ie=addToSet({},[...mathMl$1,...mathMlDisallowed]),Le=function(e){arrayPush(t.removed,{element:e});try{E(e).removeChild(e)}catch(t){g(e)}},Me=function(e,n){try{arrayPush(t.removed,{attribute:n.getAttributeNode(e),from:n})}catch(e){arrayPush(t.removed,{attribute:null,from:n})}if(n.removeAttribute(e),"is"===e)if(ee||te)try{Le(n)}catch(e){}else try{n.setAttribute(e,"")}catch(e){}},Pe=function(e){let t=null,r=null;if(Q)e="<remove></remove>"+e;else{const t=stringMatch(e,/^[\r\n\t ]+/);r=t&&t[0]}"application/xhtml+xml"===Ne&&he===ge&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=S?S.createHTML(e):e;if(he===ge)try{t=(new m).parseFromString(o,Ne)}catch(e){}if(!t||!t.documentElement){t=A.createDocument(he,"template",null);try{t.documentElement.innerHTML=ye?_:o}catch(e){}}const a=t.body||t.documentElement;return e&&r&&a.insertBefore(n.createTextNode(r),a.childNodes[0]||null),he===ge?O.call(t,Z?"html":"body")[0]:Z?t.documentElement:a},ke=function(e){return b.call(e.ownerDocument||e,e,c.SHOW_ELEMENT|c.SHOW_COMMENT|c.SHOW_TEXT|c.SHOW_PROCESSING_INSTRUCTION|c.SHOW_CDATA_SECTION,null)},ze=function(e){return e instanceof u&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof p)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},He=function(e){return"function"==typeof l&&e instanceof l};function Ue(e,n,r){arrayForEach(e,e=>{e.call(t,n,r,De)})}const Fe=function(e){let n=null;if(Ue(D.beforeSanitizeElements,e,null),ze(e))return Le(e),!0;const r=Re(e.nodeName);if(Ue(D.uponSanitizeElement,e,{tagName:r,allowedTags:z}),V&&e.hasChildNodes()&&!He(e.firstElementChild)&&regExpTest(/<[/\w!]/g,e.innerHTML)&&regExpTest(/<[/\w!]/g,e.textContent))return Le(e),!0;if(e.nodeType===NODE_TYPE.progressingInstruction)return Le(e),!0;if(V&&e.nodeType===NODE_TYPE.comment&&regExpTest(/<[/\w]/g,e.data))return Le(e),!0;if(!(j.tagCheck instanceof Function&&j.tagCheck(r))&&(!z[r]||W[r])){if(!W[r]&&We(r)){if(G.tagNameCheck instanceof RegExp&&regExpTest(G.tagNameCheck,r))return!1;if(G.tagNameCheck instanceof Function&&G.tagNameCheck(r))return!1}if(ae&&!se[r]){const t=E(e)||e.parentNode,n=y(e)||e.childNodes;if(n&&t){for(let r=n.length-1;r>=0;--r){const o=T(n[r],!0);o.__removalCount=(e.__removalCount||0)+1,t.insertBefore(o,h(e))}}}return Le(e),!0}return e instanceof s&&!function(e){let t=E(e);t&&t.tagName||(t={namespaceURI:he,tagName:"template"});const n=stringToLowerCase(e.tagName),r=stringToLowerCase(t.tagName);return!!Ee[e.namespaceURI]&&(e.namespaceURI===Te?t.namespaceURI===ge?"svg"===n:t.namespaceURI===fe?"svg"===n&&("annotation-xml"===r||_e[r]):Boolean(ve[n]):e.namespaceURI===fe?t.namespaceURI===ge?"math"===n:t.namespaceURI===Te?"math"===n&&Ae[r]:Boolean(Ie[n]):e.namespaceURI===ge?!(t.namespaceURI===Te&&!Ae[r])&&!(t.namespaceURI===fe&&!_e[r])&&!Ie[n]&&(be[n]||!ve[n]):!("application/xhtml+xml"!==Ne||!Ee[e.namespaceURI]))}(e)?(Le(e),!0):"noscript"!==r&&"noembed"!==r&&"noframes"!==r||!regExpTest(/<\/no(script|embed|frames)/i,e.innerHTML)?(K&&e.nodeType===NODE_TYPE.text&&(n=e.textContent,arrayForEach([w,C,x],e=>{n=stringReplace(n,e," ")}),e.textContent!==n&&(arrayPush(t.removed,{element:e.cloneNode()}),e.textContent=n)),Ue(D.afterSanitizeElements,e,null),!1):(Le(e),!0)},Ge=function(e,t,r){if(re&&("id"===t||"name"===t)&&(r in n||r in we))return!1;if(X&&!B[t]&&regExpTest(v,t));else if(Y&&regExpTest(I,t));else if(j.attributeCheck instanceof Function&&j.attributeCheck(t,e));else if(!U[t]||B[t]){if(!(We(e)&&(G.tagNameCheck instanceof RegExp&&regExpTest(G.tagNameCheck,e)||G.tagNameCheck instanceof Function&&G.tagNameCheck(e))&&(G.attributeNameCheck instanceof RegExp&&regExpTest(G.attributeNameCheck,t)||G.attributeNameCheck instanceof Function&&G.attributeNameCheck(t,e))||"is"===t&&G.allowCustomizedBuiltInElements&&(G.tagNameCheck instanceof RegExp&&regExpTest(G.tagNameCheck,r)||G.tagNameCheck instanceof Function&&G.tagNameCheck(r))))return!1}else if(me[t]);else if(regExpTest(k,stringReplace(r,M,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==stringIndexOf(r,"data:")||!pe[e]){if($&&!regExpTest(L,stringReplace(r,M,"")));else if(r)return!1}else;return!0},We=function(e){return"annotation-xml"!==e&&stringMatch(e,P)},Be=function(e){Ue(D.beforeSanitizeAttributes,e,null);const{attributes:n}=e;if(!n||ze(e))return;const r={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:U,forceKeepAttr:void 0};let o=n.length;for(;o--;){const a=n[o],{name:i,namespaceURI:l,value:s}=a,c=Re(i),p=s;let u="value"===i?p:stringTrim(p);if(r.attrName=c,r.attrValue=u,r.keepAttr=!0,r.forceKeepAttr=void 0,Ue(D.uponSanitizeAttribute,e,r),u=r.attrValue,!oe||"id"!==c&&"name"!==c||(Me(i,e),u="user-content-"+u),V&&regExpTest(/((--!?|])>)|<\/(style|title|textarea)/i,u)){Me(i,e);continue}if("attributename"===c&&stringMatch(u,"href")){Me(i,e);continue}if(r.forceKeepAttr)continue;if(!r.keepAttr){Me(i,e);continue}if(!q&&regExpTest(/\/>/i,u)){Me(i,e);continue}K&&arrayForEach([w,C,x],e=>{u=stringReplace(u,e," ")});const m=Re(e.nodeName);if(Ge(m,c,u)){if(S&&"object"==typeof d&&"function"==typeof d.getAttributeType)if(l);else switch(d.getAttributeType(m,c)){case"TrustedHTML":u=S.createHTML(u);break;case"TrustedScriptURL":u=S.createScriptURL(u)}if(u!==p)try{l?e.setAttributeNS(l,i,u):e.setAttribute(i,u),ze(e)?Le(e):arrayPop(t.removed)}catch(t){Me(i,e)}}else Me(i,e)}Ue(D.afterSanitizeAttributes,e,null)},je=function e(t){let n=null;const r=ke(t);for(Ue(D.beforeSanitizeShadowDOM,t,null);n=r.nextNode();)Ue(D.uponSanitizeShadowNode,n,null),Fe(n),Be(n),n.content instanceof a&&e(n.content);Ue(D.afterSanitizeShadowDOM,t,null)};return t.sanitize=function(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=null,i=null,s=null,c=null;if(ye=!e,ye&&(e="\x3c!--\x3e"),"string"!=typeof e&&!He(e)){if("function"!=typeof e.toString)throw typeErrorCreate("toString is not a function");if("string"!=typeof(e=e.toString()))throw typeErrorCreate("dirty is not a string, aborting")}if(!t.isSupported)return e;if(J||xe(n),t.removed=[],"string"==typeof e&&(ie=!1),ie){if(e.nodeName){const t=Re(e.nodeName);if(!z[t]||W[t])throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof l)o=Pe("\x3c!----\x3e"),i=o.ownerDocument.importNode(e,!0),i.nodeType===NODE_TYPE.element&&"BODY"===i.nodeName||"HTML"===i.nodeName?o=i:o.appendChild(i);else{if(!ee&&!K&&!Z&&-1===e.indexOf("<"))return S&&ne?S.createHTML(e):e;if(o=Pe(e),!o)return ee?null:ne?_:""}o&&Q&&Le(o.firstChild);const p=ke(ie?e:o);for(;s=p.nextNode();)Fe(s),Be(s),s.content instanceof a&&je(s.content);if(ie)return e;if(ee){if(te)for(c=N.call(o.ownerDocument);o.firstChild;)c.appendChild(o.firstChild);else c=o;return(U.shadowroot||U.shadowrootmode)&&(c=R.call(r,c,!0)),c}let u=Z?o.outerHTML:o.innerHTML;return Z&&z["!doctype"]&&o.ownerDocument&&o.ownerDocument.doctype&&o.ownerDocument.doctype.name&&regExpTest(DOCTYPE_NAME,o.ownerDocument.doctype.name)&&(u="<!DOCTYPE "+o.ownerDocument.doctype.name+">\n"+u),K&&arrayForEach([w,C,x],e=>{u=stringReplace(u,e," ")}),S&&ne?S.createHTML(u):u},t.setConfig=function(){xe(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),J=!0},t.clearConfig=function(){De=null,J=!1},t.isValidAttribute=function(e,t,n){De||xe({});const r=Re(e),o=Re(t);return Ge(r,o,n)},t.addHook=function(e,t){"function"==typeof t&&arrayPush(D[e],t)},t.removeHook=function(e,t){if(void 0!==t){const n=arrayLastIndexOf(D[e],t);return-1===n?void 0:arraySplice(D[e],n,1)[0]}return arrayPop(D[e])},t.removeHooks=function(e){D[e]=[]},t.removeAllHooks=function(){D=_createHooksMap()},t}var purify=createDOMPurify();export{purify as default};
    
  • nicegui/templates/index.html+8 0 modified
    @@ -113,6 +113,14 @@
           <span>{{ translations.message_too_long_body }}</span>
         </div>
         <script type="module">
    +      // Load DOMPurify for HTML sanitization, polyfill for browsers without native setHTML
    +      if (typeof Element.prototype.setHTML !== "function") {
    +        const { default: DOMPurify } = await import("dompurify");
    +        Element.prototype.setHTML = function (html) {
    +          this.innerHTML = DOMPurify.sanitize(html);
    +        };
    +      }
    +
           const app = createApp(parseElements(String.raw`{{ elements | safe }}`), {
             version: "{{ version }}",
             prefix: "{{ prefix | safe }}",
    
  • package.json+1 0 modified
    @@ -6,6 +6,7 @@
         "@tailwindcss/browser": "4.1.13",
         "@unocss/reset": "^66.6.0",
         "@unocss/runtime": "^66.6.0",
    +    "dompurify": "^3.3.1",
         "quasar": "2.18.5",
         "sass": "^1.94.0",
         "socket.io": "4.8.1",
    
  • package-lock.json+18 1 modified
    @@ -1,5 +1,5 @@
     {
    -  "name": "nicegui",
    +  "name": "nicegui-ghsa-v82v-c5x8-w282",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
    @@ -8,6 +8,7 @@
             "@tailwindcss/browser": "4.1.13",
             "@unocss/reset": "^66.6.0",
             "@unocss/runtime": "^66.6.0",
    +        "dompurify": "^3.3.1",
             "quasar": "2.18.5",
             "sass": "^1.94.0",
             "socket.io": "4.8.1",
    @@ -422,6 +423,13 @@
             "undici-types": "~7.10.0"
           }
         },
    +    "node_modules/@types/trusted-types": {
    +      "version": "2.0.7",
    +      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
    +      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
    +      "license": "MIT",
    +      "optional": true
    +    },
         "node_modules/@unocss/core": {
           "version": "66.6.0",
           "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.6.0.tgz",
    @@ -762,6 +770,15 @@
             "node": ">=0.10"
           }
         },
    +    "node_modules/dompurify": {
    +      "version": "3.3.1",
    +      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
    +      "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
    +      "license": "(MPL-2.0 OR Apache-2.0)",
    +      "optionalDependencies": {
    +        "@types/trusted-types": "^2.0.7"
    +      }
    +    },
         "node_modules/engine.io": {
           "version": "6.6.4",
           "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
    
  • .pre-commit-config.yaml+1 0 modified
    @@ -29,6 +29,7 @@ repos:
                 nicegui/static/vue\..*|
                 nicegui/static/quasar\..*|
                 nicegui/static/sass\..*|
    +            nicegui/static/dompurify\..*|
                 nicegui/static/unocss/.*|
                 nicegui/translations\.py|
                 website/static/fuse\.js\@.*|
    
  • tests/test_chat_message.py+14 4 modified
    @@ -1,4 +1,3 @@
    -import pytest
     from html_sanitizer import Sanitizer
     from selenium.webdriver.common.by import By
     
    @@ -19,8 +18,8 @@ def page():
                             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)
    +        ui.chat_message('80&euro;', text_html=True)
    +        ui.chat_message('90&euro;', text_html=True, sanitize=False)
     
         screen.allowed_js_errors.append('/x - Failed to load resource')
         screen.open('/')
    @@ -31,7 +30,8 @@ def page():
         screen.should_contain('50€')
         screen.should_contain('60EUR')
         screen.should_not_contain('70€')
    -    screen.should_not_contain('80€')
    +    screen.should_contain('80€')
    +    screen.should_contain('90€')
     
     
     def test_newline(screen: Screen):
    @@ -51,3 +51,13 @@ def page():
     
         screen.open('/')
         screen.should_contain('slot')
    +
    +
    +def test_xss_sanitization(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.chat_message('<img src=x onerror="alert(\'XSS\')">', text_html=True)
    +
    +    screen.allowed_js_errors.append('/x - Failed to load resource')
    +    screen.open('/')
    +    assert screen.find_by_tag('img').get_attribute('onerror') is None
    
  • tests/test_html.py+10 0 modified
    @@ -37,3 +37,13 @@ def page():
         screen.should_contain('B')
         screen.should_contain('C!')
         screen.should_not_contain('D')
    +
    +
    +def test_xss_sanitization(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.html('<img src=x onerror="alert(\'XSS\')">')
    +
    +    screen.allowed_js_errors.append('/x - Failed to load resource')
    +    screen.open('/')
    +    assert screen.find_by_tag('img').get_attribute('onerror') is None
    
  • tests/test_interactive_image.py+9 0 modified
    @@ -119,3 +119,12 @@ def page():
         with screen.implicitly_wait(0.5):
             assert len(screen.find_all_by_tag('rect')) == 1
             assert len(screen.find_all_by_tag('circle')) == 1
    +
    +
    +def test_xss_sanitization(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.interactive_image(size=(100, 100), content='<rect width="100" height="100" onclick="alert(\'XSS\')" />')
    +
    +    screen.open('/')
    +    assert screen.find_all_by_tag('rect')[0].get_attribute('onclick') is None
    
  • tests/test_markdown.py+10 0 modified
    @@ -101,3 +101,13 @@ def replace():
         screen.click('Replace')
         screen.should_contain('B')
         screen.should_not_contain('A')
    +
    +
    +def test_xss_sanitization(screen: Screen):
    +    @ui.page('/')
    +    def page():
    +        ui.markdown('<img src=x onerror="alert(\'XSS\')">')
    +
    +    screen.allowed_js_errors.append('/x - Failed to load resource')
    +    screen.open('/')
    +    assert screen.find_by_tag('img').get_attribute('onerror') is None
    

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.