Skip to content
ShineUI
Install
ShineUI / Security

Security

What the elements guard for you, and what your page still has to do.

Every element writes the strings you give it as text, so a value from your data never becomes markup. Two elements take HTML on purpose, the rich text editor and the edit box, and they keep only what their own schema allows. Three things stay with you: the URLs you pass in, the HTML your server stores, and the policy your page sends.

Text, not HTML

Every string prop and every string a render callback returns is set as text. A customer named <img src=x onerror=alert(1)> shows the brackets; nothing runs.

A render callback returns text or a DOM node, never an HTML string. The callbacks are a column's render on sui-data-table, sui-data-grid and sui-tree-table; itemRender on sui-list and sui-data-view; cardRender on sui-board; chipRender and agendaRender on sui-calendar; renderItem on sui-virtual-scroller; an option's render on sui-select and sui-multi-select; and an item's render on sui-timeline. To show markup, build the node yourself and fill it with textContent, never innerHTML.

table.columns = [ { key: "name", header: "Customer", render: (row) => { const link = document.createElement("a"); link.href = safeUrl(row.profileUrl); link.textContent = row.name; return link; }, }, ];

URLs are written as given

A URL you hand an element goes into the page as you gave it, as it would on a plain <a href>: a nav item, a breadcrumb, a menu row, a card, a list row, a button or icon button with an href, an avatar, an image, the upload endpoint. The element does not filter it, so a javascript: URL in an href runs when the reader clicks it. Filter a URL that came from a user once, where it enters your app, and keep only the schemes you expect. Where an element offers target, a new tab also gets rel="noopener".

const ALLOWED = new Set(["http:", "https:", "mailto:", "tel:"]); export function safeUrl(value) { try { const url = new URL(value, window.location.href); return ALLOWED.has(url.protocol) ? url.href : "about:blank"; } catch { return "about:blank"; } }

Editor HTML

sui-rich-text-editor and sui-edit-box read HTML in an inert document, where no script runs and nothing loads, and keep only the tags, attributes and URLs their schema can produce. A link keeps an http, https, mailto, tel or sms address or a relative path. A picture keeps an http, https or blob address or a data:image/ URL. A paste is held to the same rule.

That protects the page that shows the editor. It does not protect your server, because any client can post any string. Sanitize the HTML on the server before you store it, and again wherever you show it outside the editor. Use an allowlist sanitizer such as DOMPurify (on jsdom) or sanitize-html, and allow what the editor writes: p, h1 to h6, blockquote, pre, code, hr, br, ul, ol with start, li, a with href and title, img with src, alt and title, strong, b, em, i, u, s, del, and the span a merge field is drawn with.

Pictures in editor content load with referrerpolicy="no-referrer", so an image host does not learn which page showed them.

Content security policy

Every element writes its styles into a <style> element. Under a style-src with no 'unsafe-inline', those styles need the page's nonce. Generate a fresh nonce for every response, send it in the header, and write the same value into a meta tag in the head. The elements read the meta tag for their own styles, and applyTheme reads it for the theme sheet it writes. setNonce, on Get started, does the same from code.

// On the server, once per response import { randomBytes } from "node:crypto"; const nonce = randomBytes(16).toString("base64"); response.setHeader("Content-Security-Policy", [ "default-src 'self'", "script-src 'self'", `style-src 'self' 'nonce-${nonce}'`, "img-src 'self' blob:", "font-src 'self'", "object-src 'none'", "base-uri 'self'", "require-trusted-types-for 'script'", "trusted-types shineui ProseMirrorClipboard", ].join("; "));
<head> <meta name="csp-nonce" content="NONCE_FROM_THE_SERVER"> ... </head>
SourceWhy the elements need it
style-src 'nonce-…'The style elements the elements write and the theme sheet from applyTheme carry the nonce. Inline style attributes are set through the style object, which the policy allows.
script-src 'self'The loader and the element chunks are files on your origin. Nothing needs an inline script or 'unsafe-eval'.
img-src blob:sui-file-upload draws a thumbnail of a picked image from an object URL. Add data: only if you pass data URLs yourself, to an avatar or in editor content, and add the hosts your own images come from.
font-src 'self'The fonts sheet loads DM Sans from beside itself and nothing from another host. Font Awesome is yours to provide: serve @fortawesome/fontawesome-free from your origin, or add its CDN host to style-src and font-src and pin the sheet with an integrity hash.
trusted-typesThe two policy names below.

The library's browser tests load the elements under this policy in Chromium, Firefox and WebKit, with and without the two Trusted Types lines, and fail on any violation. A page that loads the elements from a CDN adds that host to script-src, style-src and font-src. On a server-rendered page the server reads the same meta tag: see Server rendering.

Trusted Types

Under require-trusted-types-for 'script', an HTML string cannot reach innerHTML or DOMParser without a policy. The library creates one policy, named shineui, on first use and only where the browser has Trusted Types. It wraps two strings, both already safe: the editors' parse into an inert document, and the edit box showing HTML its sanitizer has already reduced to the allowlist. The rich text editor's paste goes through ProseMirror's own policy, ProseMirrorClipboard. List both names in trusted-types. No other element writes HTML.

If two copies of the library load on one page, the second cannot create the policy again. Add 'allow-duplicates' to the directive, or load one copy.

Clipboard and exports

A cell whose text starts with =, +, -, @, a tab or a line break is a formula to a spreadsheet, unless it is a plain number such as -12. A formula such as =HYPERLINK(…) from your data can act when the reader opens the sheet. The CSV export of sui-data-table and sui-data-grid escapes such text with a leading apostrophe by default, and so does a copy or a cut from sui-data-grid. Set copyFormulas on the grid when your users copy formulas on purpose. A copy also carries the grid's own clipboard format, so a paste into a grid on the same page arrives exactly as it left; text from anywhere else keeps its apostrophes.

File uploads

accept and maxFileSize on sui-file-upload shape what the picker offers and what the list accepts. They help the reader; they do not protect your server, because anything can be posted to the endpoint. Check the type, the size and the content of every file on the server.

A thumbnail is an object URL of the picked file. The element revokes it when the row goes or the host replaces files.

Reporting a vulnerability

Email info@shineui.dev with what you found and how to reproduce it, and keep it out of public channels.