From deac631d32b755a58df18ae9bebd9dc320cf5b90 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 13 May 2026 22:18:54 -0400 Subject: [PATCH] feat: use new Sanitizer API & ditch stripScripts() --- lerna.json | 4 +++ package.json | 2 +- packages/demo/src/options/options28.ts | 5 +-- packages/demo/src/options/options32.html | 15 +-------- packages/demo/src/options/options32.ts | 31 +++++++++++++------ .../src/MultipleSelectInstance.ts | 4 +-- .../multiple-select-vanilla/src/constants.ts | 17 ++++++++++ packages/multiple-select-vanilla/src/index.ts | 1 - .../src/utils/utils.ts | 7 ----- playwright/e2e/options32.spec.ts | 7 +++-- 10 files changed, 53 insertions(+), 40 deletions(-) diff --git a/lerna.json b/lerna.json index 32e665d94..286793577 100644 --- a/lerna.json +++ b/lerna.json @@ -20,6 +20,10 @@ "commentPullRequests": "🎉 This pull request is included in version %v 📦
🔗 The release notes are available at: [GitHub Release](%u) 🚀", "message": "chore(release): publish new version %s", "releaseFooterMessage": "
🎉 Another great release available on GitHub and NPM 🤖. Star us on GitHub ⭐" + }, + "watch": { + "noBail": true, + "noShell": true } }, "changelogPreset": "conventionalcommits", diff --git a/package.json b/package.json index c5413e359..69dc348a2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "build:lib": "pnpm -r --stream --filter=\"{packages/multiple-select-vanilla/**}\" build", "dev": "pnpm -r dev:init && run-p dev:watch build:watch --npm-path pnpm", "dev:watch": "pnpm -r --parallel --stream dev", - "build:watch": "lerna watch --no-bail --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" -- cross-env-shell pnpm -r --filter $LERNA_PACKAGE_NAME build:watch --files=$LERNA_FILE_CHANGES", + "build:watch": "lerna watch --file-delimiter=\",\" --glob=\"src/**/*.{ts,scss}\" -- cross-env-shell 'pnpm -r --filter $LERNA_PACKAGE_NAME build:watch --files=$LERNA_FILE_CHANGES'", "dev:demo": "pnpm -r --stream --filter=\"{packages/demo/**}\" dev", "dev:lib": "pnpm -r --stream --filter=\"{packages/multiple-select-vanilla/**}\" dev", "biome:lint:check": "biome lint ./packages", diff --git a/packages/demo/src/options/options28.ts b/packages/demo/src/options/options28.ts index 1cc3f2334..81027565e 100644 --- a/packages/demo/src/options/options28.ts +++ b/packages/demo/src/options/options28.ts @@ -1,4 +1,4 @@ -import DOMPurify from 'dompurify'; +// import DOMPurify from 'dompurify'; import { type MultipleSelectInstance, multipleSelect } from 'multiple-select-vanilla'; export default class Example { @@ -10,7 +10,8 @@ export default class Example { labelTemplate: el => { return `${el.getAttribute('label')}`; }, - sanitizer: html => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }), + // default sanitizer uses the Sanitizer API: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API + // sanitizer: html => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }), }) as MultipleSelectInstance; } diff --git a/packages/demo/src/options/options32.html b/packages/demo/src/options/options32.html index eb61c2920..74c940da5 100644 --- a/packages/demo/src/options/options32.html +++ b/packages/demo/src/options/options32.html @@ -27,20 +27,7 @@

- +
diff --git a/packages/demo/src/options/options32.ts b/packages/demo/src/options/options32.ts index 8b72fd528..a83fff4fe 100644 --- a/packages/demo/src/options/options32.ts +++ b/packages/demo/src/options/options32.ts @@ -1,3 +1,4 @@ +import DOMPurify from 'dompurify'; import { type MultipleSelectInstance, multipleSelect } from 'multiple-select-vanilla'; export default class Example { @@ -5,17 +6,27 @@ export default class Example { mount() { this.ms1 = multipleSelect('#select1', { - placeholder: 'Placeholder with cross-site scripting code...', - sanitizer: (dirtyHtml: string) => - typeof dirtyHtml === 'string' - ? decodeURIComponent(dirtyHtml).replace( - /(\b)(on[a-z]+)(\s*)=|javascript:([^>]*)[^>]*|(<\s*)(\/*)script([<>]*).*(<\s*)(\/*)script(>*)|(<)(\/*)(script|script defer)(.*)(>|>">)/gi, - '', - ) - : dirtyHtml, + data: [ + { + value: 'Safe HTML value', + text: '1. Safe HTML example', + }, + { + value: 'Blocked by stripScripts', + text: '2. Payload blocked by stripScripts', + }, + { + value: '', + text: '3. Payload that bypasses stripScripts and executes', + }, + ], + filter: true, + placeholder: "Placeholder with cross-site scripting code...<script\>alert('XSS')<\/script>", + useSelectOptionLabelToHtml: true, - // or even better, use dedicated libraries like DOM Purify: https://github.com/cure53/DOMPurify - // sanitizer: (html) => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }), + // you can use DOMPurify to sanitize data + // the default sanitizer is the Sanitizer API: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API + sanitizer: html => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true }), }) as MultipleSelectInstance; } diff --git a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts index 84aa39212..933744e85 100644 --- a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts +++ b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts @@ -22,7 +22,7 @@ import { insertAfter, toggleElement, } from './utils/domUtils.js'; -import { compareObjects, deepCopy, findByParam, removeDiacritics, removeUndefined, setDataKeys, stripScripts } from './utils/utils.js'; +import { compareObjects, deepCopy, findByParam, removeDiacritics, removeUndefined, setDataKeys } from './utils/utils.js'; const OPTIONS_LIST_SELECTOR = '.ms-select-all, ul li[data-key]'; const OPTIONS_HIGHLIGHT_LIST_SELECTOR = '.ms-select-all.highlighted, ul li[data-key].highlighted'; @@ -1532,7 +1532,7 @@ export class MultipleSelectInstance { const getSelectOptionHtml = () => { if (this.options.useSelectOptionLabel || this.options.useSelectOptionLabelToHtml) { const labels = valueSelects.join(this.options.displayDelimiter); - return this.options.useSelectOptionLabelToHtml ? stripScripts(labels) : labels; + return labels; } return textSelects.join(this.options.displayDelimiter); }; diff --git a/packages/multiple-select-vanilla/src/constants.ts b/packages/multiple-select-vanilla/src/constants.ts index 2b6e1381d..88493a9fd 100644 --- a/packages/multiple-select-vanilla/src/constants.ts +++ b/packages/multiple-select-vanilla/src/constants.ts @@ -85,6 +85,23 @@ const DEFAULTS: Partial = { onDestroy: noopFalse, onAfterDestroy: noopFalse, onDestroyed: noopFalse, + sanitizer: text => { + if ('setHTML' in Element.prototype) { + const container = document.createElement('div'); + // @ts-ignore: experimental API + container.setHTML(text, { + sanitizer: new Sanitizer({ + // let's add the most common elements & attributes + // also see: https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API/Default_sanitizer_configuration + elements: ['i', 'span', 'div', 'p', 'b', 'strong', 'em', 'br', 'ul', 'ol', 'li', 'a', 'img'], + attributes: ['class', 'title', 'alt', 'src', 'href', 'target', 'rel', 'width', 'height', 'level'], + replaceWithChildrenElements: [], + }), + }); + return container.innerHTML; + } + return text; + }, }; const METHODS = [ diff --git a/packages/multiple-select-vanilla/src/index.ts b/packages/multiple-select-vanilla/src/index.ts index f39293ff4..a2702143a 100644 --- a/packages/multiple-select-vanilla/src/index.ts +++ b/packages/multiple-select-vanilla/src/index.ts @@ -30,5 +30,4 @@ export { removeDiacritics, removeUndefined, setDataKeys, - stripScripts, } from './utils/utils.js'; diff --git a/packages/multiple-select-vanilla/src/utils/utils.ts b/packages/multiple-select-vanilla/src/utils/utils.ts index f3a08c082..dd85bdcc9 100644 --- a/packages/multiple-select-vanilla/src/utils/utils.ts +++ b/packages/multiple-select-vanilla/src/utils/utils.ts @@ -104,13 +104,6 @@ export function findByParam(data: any, param: any, value: any) { } } -export function stripScripts(dirtyHtml: string) { - return dirtyHtml.replace( - /(\b)(on[a-z]+)(\s*)=([^>]*)|javascript:([^>]*)[^>]*|(<\s*)(\/*)script([<>]*).*(<\s*)(\/*)script(>*)|(<|<)(\/*)(script|script defer)(.*)(>|>|>">)/gi, - '', - ); -} - export function removeUndefined = Record>(obj: T): T { Object.keys(obj).forEach(key => (!isDefined(obj[key]) ? delete obj[key] : '')); return obj; diff --git a/playwright/e2e/options32.spec.ts b/playwright/e2e/options32.spec.ts index b9470bfd5..5baa36839 100644 --- a/playwright/e2e/options32.spec.ts +++ b/playwright/e2e/options32.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; test.describe('Options 32 - Sanitizer', () => { - test('select shows image not found and JS alert should be sanitized and not trigger', async ({ page }) => { + test('select last 2 options should not trigger any alert(XSS)', async ({ page }) => { let alertTriggered = false; page.on('dialog', async alert => { alertTriggered = true; @@ -11,8 +11,9 @@ test.describe('Options 32 - Sanitizer', () => { await page.goto('#/options32'); await page.locator('.ms-parent', { hasText: 'Placeholder with cross-site scripting code...' }).click(); - await page.locator('span').filter({ hasText: 'February' }).click(); - await page.locator('span').filter({ hasText: 'March' }).click(); + await page.locator('span').filter({ hasText: '1. Safe HTML example' }).click(); + await page.locator('span').filter({ hasText: '2. Payload blocked by stripScripts' }).click(); + await page.locator('span').filter({ hasText: '3. Payload that bypasses stripScripts and executes' }).click(); await expect(alertTriggered).toBeFalsy(); }); });