Skip to content

Commit 308a5fe

Browse files
committed
feat: add sanitizer callback option to sanitize html code
1 parent 16379f8 commit 308a5fe

File tree

7 files changed

+89
-5
lines changed

7 files changed

+89
-5
lines changed

demo/src/app-routing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import Options28 from './options/options28';
4141
import Options29 from './options/options29';
4242
import Options30 from './options/options30';
4343
import Options31 from './options/options31';
44+
import Options32 from './options/options32';
4445
import Methods01 from './methods/methods01';
4546
import Methods02 from './methods/methods02';
4647
import Methods03 from './methods/methods03';
@@ -111,6 +112,7 @@ export const exampleRouting = [
111112
{ name: 'options29', view: '/src/options/options29.html', viewModel: Options29, title: 'Auto-Adjust Drop Position' },
112113
{ name: 'options30', view: '/src/options/options30.html', viewModel: Options30, title: 'Auto-Adjust Drop Height/Width' },
113114
{ name: 'options31', view: '/src/options/options31.html', viewModel: Options31, title: 'Use Select Option as Label' },
115+
{ name: 'options32', view: '/src/options/options32.html', viewModel: Options32, title: 'Sanitizer' },
114116
],
115117
},
116118
{

demo/src/options/options32.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<div class="row mb-2">
2+
<div class="col-md-12 title-desc">
3+
<h2 class="bd-title">
4+
Sanitizer
5+
<span class="float-end links">
6+
Code <span class="fa fa-link"></span>
7+
<span class="small">
8+
<a
9+
target="_blank"
10+
href="https://github.com/ghiscoding/multiple-select-vanilla/blob/main/demo/src/options/options08.html"
11+
>html</a
12+
>
13+
|
14+
<a target="_blank" href="https://github.com/ghiscoding/multiple-select-vanilla/blob/main/demo/src/options/options08.ts"
15+
>ts</a
16+
>
17+
</span>
18+
</span>
19+
</h2>
20+
<div class="demo-subtitle">
21+
Use <code>sanitizer</code> callback option to sanitize all html code and prevent cross-site scripting attack.
22+
</div>
23+
</div>
24+
</div>
25+
26+
<div>
27+
<div class="mb-3 row">
28+
<label class="col-sm-3 text-end">Select placeholder with XSS</label>
29+
30+
<div class="col-sm-9">
31+
<select id="select1" multiple="multiple" class="full-width">
32+
<option value="1">January</option>
33+
<option value="2">February</option>
34+
<option value="3">March</option>
35+
<option value="4">April</option>
36+
<option value="5">May</option>
37+
<option value="6">June</option>
38+
<option value="7">July</option>
39+
<option value="8">August</option>
40+
<option value="9">September</option>
41+
<option value="10">October</option>
42+
<option value="11">November</option>
43+
<option value="12">December</option>
44+
</select>
45+
</div>
46+
</div>
47+
</div>

demo/src/options/options32.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { multipleSelect, MultipleSelectInstance } from 'multiple-select-vanilla';
2+
3+
export default class Example {
4+
ms1?: MultipleSelectInstance;
5+
6+
mount() {
7+
this.ms1 = multipleSelect('#select1', {
8+
placeholder: 'Placeholder with cross-site scripting code...<img src="not-found" onerror=alert("Hacked")>',
9+
sanitizer: (dirtyHtml: string) =>
10+
typeof dirtyHtml === 'string'
11+
? // prettier-ignore
12+
decodeURIComponent(dirtyHtml).replace(/(\b)(on[a-z]+)(\s*)=|javascript:([^>]*)[^>]*|(<\s*)(\/*)script([<>]*).*(<\s*)(\/*)script(>*)|(&lt;)(\/*)(script|script defer)(.*)(&gt;|&gt;">)/gi, '')
13+
: dirtyHtml,
14+
15+
// or even better, use dedicated libraries like DOM Purify: https://github.com/cure53/DOMPurify
16+
// sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml || '')
17+
}) as MultipleSelectInstance;
18+
}
19+
20+
unmount() {
21+
// destroy ms instance(s) to avoid DOM leaks
22+
this.ms1?.destroy();
23+
this.ms1 = undefined;
24+
}
25+
}

lib/src/MultipleSelectInstance.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ export class MultipleSelectInstance {
427427
rows,
428428
scrollEl: this.ulElm,
429429
contentEl: this.ulElm,
430+
sanitizer: this.options.sanitizer,
430431
callback: () => {
431432
updateDataOffset();
432433
this.events();
@@ -442,7 +443,7 @@ export class MultipleSelectInstance {
442443
}
443444
} else {
444445
if (this.ulElm) {
445-
this.ulElm.innerHTML = rows.join('');
446+
this.ulElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(rows.join('')) : rows.join('');
446447
this.updateDataStart = 0;
447448
this.updateDataEnd = this.updateData.length;
448449
this.virtualScroll = null;
@@ -894,8 +895,9 @@ export class MultipleSelectInstance {
894895

895896
if (spanElm) {
896897
if (sl === 0) {
898+
const placeholder = this.options.placeholder || '';
897899
spanElm.classList.add('ms-placeholder');
898-
spanElm.innerHTML = this.options.placeholder || '';
900+
spanElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(placeholder) : placeholder;
899901
} else if (sl < this.options.minimumCountSelected) {
900902
html = getSelectOptionHtml();
901903
} else if (this.options.formatAllSelected() && sl === this.dataTotal) {
@@ -911,7 +913,7 @@ export class MultipleSelectInstance {
911913
if (html) {
912914
spanElm?.classList.remove('ms-placeholder');
913915
if (this.options.useSelectOptionLabelToHtml) {
914-
spanElm.innerHTML = html;
916+
spanElm.innerHTML = this.options.sanitizer ? this.options.sanitizer(html) : html;
915917
} else {
916918
spanElm.textContent = html;
917919
}

lib/src/interfaces/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export interface VirtualScrollOption {
2020
scrollEl: HTMLElement;
2121
contentEl: HTMLElement;
2222
callback: () => void;
23+
sanitizer?: (dirtyHtml: string) => string;
2324
}

lib/src/interfaces/multipleSelectOption.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ export interface MultipleSelectOption {
207207

208208
/** Fires when a checkbox filter is changed. */
209209
onFilter: (text?: string) => void;
210+
211+
/**
212+
* Sanitizes HTML code, for example `<script>`, to prevents XSS attacks.
213+
* A library like DOM Purify could be used to sanitize html code, for example: `sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml || '')`
214+
*/
215+
sanitizer?: (dirtyHtml: string) => string;
210216
}
211217

212218
export interface MultipleSelectView {

lib/src/services/virtual-scroll.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class VirtualScroll {
1717
scrollTop: number;
1818
destroy: () => void;
1919
callback: () => void;
20+
sanitizer?: (dirtyHtml: string) => string;
2021

2122
constructor(options: VirtualScrollOption) {
2223
this.rows = options.rows;
@@ -51,7 +52,7 @@ export class VirtualScroll {
5152
if (typeof this.clusterHeight === 'undefined') {
5253
this.cache.scrollTop = this.scrollEl.scrollTop;
5354
const data = rows[0] + rows[0] + rows[0];
54-
this.contentEl.innerHTML = `${data}`;
55+
this.contentEl.innerHTML = this.sanitizer ? this.sanitizer(`${data}`) : `${data}`;
5556
this.cache.data = data;
5657
this.getRowsHeight();
5758
}
@@ -71,7 +72,7 @@ export class VirtualScroll {
7172
if (data.bottomOffset) {
7273
html.push(this.getExtra('bottom', data.bottomOffset));
7374
}
74-
this.contentEl.innerHTML = html.join('');
75+
this.contentEl.innerHTML = this.sanitizer ? this.sanitizer(html.join('')) : html.join('');
7576
} else if (bottomOffsetChanged && this.contentEl.lastChild) {
7677
(this.contentEl.lastChild as HTMLElement).style.height = `${data.bottomOffset}px`;
7778
}

0 commit comments

Comments
 (0)