NiceGUI's XSS vulnerability in ui.markdown() allows arbitrary JavaScript execution through unsanitized HTML content
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.
| Package | Affected versions | Patched versions |
|---|---|---|
niceguiPyPI | < 3.7.0 | 3.7.0 |
Affected products
1- Range: < 3.7.0
Patches
1f1f753357787Merge commit from fork
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)&®ExpTest(/<[/\w!]/g,e.innerHTML)&®ExpTest(/<[/\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&®ExpTest(/<[/\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&®ExpTest(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]&®ExpTest(v,t));else if(Y&®ExpTest(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&®ExpTest(G.tagNameCheck,e)||G.tagNameCheck instanceof Function&&G.tagNameCheck(e))&&(G.attributeNameCheck instanceof RegExp&®ExpTest(G.attributeNameCheck,t)||G.attributeNameCheck instanceof Function&&G.attributeNameCheck(t,e))||"is"===t&&G.allowCustomizedBuiltInElements&&(G.tagNameCheck instanceof RegExp&®ExpTest(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&®ExpTest(/((--!?|])>)|<\/(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&®ExpTest(/\/>/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&®ExpTest(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('€', 'EUR')) ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"70€"})>', text_html=True, sanitize=Sanitizer().sanitize) - with pytest.raises(ValueError): - ui.chat_message('80€', text_html=True) + ui.chat_message('80€', text_html=True) + ui.chat_message('90€', 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- github.com/advisories/GHSA-v82v-c5x8-w282ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25516ghsaADVISORY
- github.com/zauberzeug/nicegui/commit/f1f7533577875af7d23f161ed3627f73584cb561ghsax_refsource_MISCWEB
- github.com/zauberzeug/nicegui/security/advisories/GHSA-v82v-c5x8-w282ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.