From 30e5bdcf3a3ff78de8aa386303becbe22e64ba28 Mon Sep 17 00:00:00 2001 From: Grug Date: Fri, 23 Oct 2026 09:23:05 +0800 Subject: [PATCH] Escape toast text by default, add `html` option (#203) Toast title, description and action labels were inserted into the page as HTML, so passing user input could inject markup or script. They are now escaped by default, and `html: true` keeps the old raw-HTML behaviour. The action href, onclick strings, category and duration are escaped as attribute values. `icon` stays raw markup. Documents the `html` and `icon` options, adds a warning about `html` and `onclick`, and notes that the Jinja and Nunjucks macros rely on template autoescaping. Co-authored-by: Maurits van Riezen <12109031+mousetail@users.noreply.github.com> --- site/src/docs/components/toast.mdx | 10 ++++++++++ src/js/toast.js | 25 ++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/site/src/docs/components/toast.mdx b/site/src/docs/components/toast.mdx index 6a94839..b9e8bb6 100644 --- a/site/src/docs/components/toast.mdx +++ b/site/src/docs/components/toast.mdx @@ -164,6 +164,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip ### Config object + + `title`, `description`, and action labels are inserted as plain text. With `html: true` they are inserted as HTML, so never pass untrusted user input with it. `onclick` is JavaScript code and must never contain user input. `icon` is always inserted as raw HTML (it is meant for SVG markup). + +
duration Optional
Timeout in milliseconds. Defaults to 3000, or 5000 for error toasts. Use -1 to keep it open.
@@ -177,4 +181,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
Primary action. Supports label with either href or onclick.
cancel Optional
Secondary action. Supports label and optional onclick.
+
icon Optional
+
Custom icon markup, usually an SVG. Inserted as raw HTML. Defaults to the category icon.
+
html Optional
+
Set to true to insert title, description, and action labels as HTML instead of plain text. Defaults to false. Only use it with trusted content.
+ +The Jinja and Nunjucks `toast()` macros render on your server, so your template engine's autoescaping applies to `title`, `description`, and labels there when it is enabled. Nunjucks enables it by default. With Jinja, make sure your environment's `autoescape` setting covers the Basecoat template files (for example `.html.jinja`). `icon` is output with `safe`, and `onclick` is output as an attribute, so neither should contain user input. diff --git a/src/js/toast.js b/src/js/toast.js index 10c0959..88cdf58 100644 --- a/src/js/toast.js +++ b/src/js/toast.js @@ -131,6 +131,13 @@ element.addEventListener('transitionend', () => element.remove(), { once: true }); } + const escapeHtml = (value) => String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + function createToast(config) { const { category = 'info', @@ -140,18 +147,22 @@ cancel, duration, icon, + html: allowHtml = false, } = config; + // Text is escaped unless `html: true`. `icon` is always raw markup (SVG). + const text = (value) => (allowHtml ? String(value) : escapeHtml(value)); + const iconHtml = icon || (category && ICONS[category]) || ''; - const titleHtml = title ? `

${title}

` : ''; - const descriptionHtml = description ? `

${description}

` : ''; + const titleHtml = title ? `

${text(title)}

` : ''; + const descriptionHtml = description ? `

${text(description)}

` : ''; const actionHtml = action?.href - ? `${action.label}` + ? `${text(action.label)}` : action?.onclick - ? `` + ? `` : ''; const cancelHtml = cancel - ? `` + ? `` : ''; const footerHtml = actionHtml || cancelHtml ? `` : ''; @@ -161,8 +172,8 @@ class="toast" role="${category === 'error' ? 'alert' : 'status'}" aria-atomic="true" - ${category ? `data-category="${category}"` : ''} - ${duration !== undefined ? `data-duration="${duration}"` : ''} + ${category ? `data-category="${escapeHtml(category)}"` : ''} + ${duration !== undefined ? `data-duration="${escapeHtml(duration)}"` : ''} >
${iconHtml}