diff --git a/lerna.json b/lerna.json
index 32e665d9..28679357 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 c5413e35..69dc348a 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 1cc3f233..81027565 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 eb61c292..74c940da 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 8b72fd52..a83fff4f 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 84aa3921..933744e8 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 2b6e1381..88493a9f 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 f39293ff..a2702143 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 f3a08c08..dd85bdcc 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 b9470bfd..5baa3683 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();
});
});