1 | /**
|
---|
2 | * --------------------------------------------------------------------------
|
---|
3 | * Bootstrap (v5.1.3): util/sanitizer.js
|
---|
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
---|
5 | * --------------------------------------------------------------------------
|
---|
6 | */
|
---|
7 |
|
---|
8 | const uriAttributes = new Set([
|
---|
9 | 'background',
|
---|
10 | 'cite',
|
---|
11 | 'href',
|
---|
12 | 'itemtype',
|
---|
13 | 'longdesc',
|
---|
14 | 'poster',
|
---|
15 | 'src',
|
---|
16 | 'xlink:href'
|
---|
17 | ])
|
---|
18 |
|
---|
19 | const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
|
---|
20 |
|
---|
21 | /**
|
---|
22 | * A pattern that recognizes a commonly useful subset of URLs that are safe.
|
---|
23 | *
|
---|
24 | * Shoutout to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts
|
---|
25 | */
|
---|
26 | const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i
|
---|
27 |
|
---|
28 | /**
|
---|
29 | * A pattern that matches safe data URLs. Only matches image, video and audio types.
|
---|
30 | *
|
---|
31 | * Shoutout to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts
|
---|
32 | */
|
---|
33 | const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i
|
---|
34 |
|
---|
35 | const allowedAttribute = (attribute, allowedAttributeList) => {
|
---|
36 | const attributeName = attribute.nodeName.toLowerCase()
|
---|
37 |
|
---|
38 | if (allowedAttributeList.includes(attributeName)) {
|
---|
39 | if (uriAttributes.has(attributeName)) {
|
---|
40 | return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue))
|
---|
41 | }
|
---|
42 |
|
---|
43 | return true
|
---|
44 | }
|
---|
45 |
|
---|
46 | const regExp = allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
|
---|
47 |
|
---|
48 | // Check if a regular expression validates the attribute.
|
---|
49 | for (let i = 0, len = regExp.length; i < len; i++) {
|
---|
50 | if (regExp[i].test(attributeName)) {
|
---|
51 | return true
|
---|
52 | }
|
---|
53 | }
|
---|
54 |
|
---|
55 | return false
|
---|
56 | }
|
---|
57 |
|
---|
58 | export const DefaultAllowlist = {
|
---|
59 | // Global attributes allowed on any supplied element below.
|
---|
60 | '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
|
---|
61 | a: ['target', 'href', 'title', 'rel'],
|
---|
62 | area: [],
|
---|
63 | b: [],
|
---|
64 | br: [],
|
---|
65 | col: [],
|
---|
66 | code: [],
|
---|
67 | div: [],
|
---|
68 | em: [],
|
---|
69 | hr: [],
|
---|
70 | h1: [],
|
---|
71 | h2: [],
|
---|
72 | h3: [],
|
---|
73 | h4: [],
|
---|
74 | h5: [],
|
---|
75 | h6: [],
|
---|
76 | i: [],
|
---|
77 | img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
|
---|
78 | li: [],
|
---|
79 | ol: [],
|
---|
80 | p: [],
|
---|
81 | pre: [],
|
---|
82 | s: [],
|
---|
83 | small: [],
|
---|
84 | span: [],
|
---|
85 | sub: [],
|
---|
86 | sup: [],
|
---|
87 | strong: [],
|
---|
88 | u: [],
|
---|
89 | ul: []
|
---|
90 | }
|
---|
91 |
|
---|
92 | export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) {
|
---|
93 | if (!unsafeHtml.length) {
|
---|
94 | return unsafeHtml
|
---|
95 | }
|
---|
96 |
|
---|
97 | if (sanitizeFn && typeof sanitizeFn === 'function') {
|
---|
98 | return sanitizeFn(unsafeHtml)
|
---|
99 | }
|
---|
100 |
|
---|
101 | const domParser = new window.DOMParser()
|
---|
102 | const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
|
---|
103 | const elements = [].concat(...createdDocument.body.querySelectorAll('*'))
|
---|
104 |
|
---|
105 | for (let i = 0, len = elements.length; i < len; i++) {
|
---|
106 | const element = elements[i]
|
---|
107 | const elementName = element.nodeName.toLowerCase()
|
---|
108 |
|
---|
109 | if (!Object.keys(allowList).includes(elementName)) {
|
---|
110 | element.remove()
|
---|
111 |
|
---|
112 | continue
|
---|
113 | }
|
---|
114 |
|
---|
115 | const attributeList = [].concat(...element.attributes)
|
---|
116 | const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])
|
---|
117 |
|
---|
118 | attributeList.forEach(attribute => {
|
---|
119 | if (!allowedAttribute(attribute, allowedAttributes)) {
|
---|
120 | element.removeAttribute(attribute.nodeName)
|
---|
121 | }
|
---|
122 | })
|
---|
123 | }
|
---|
124 |
|
---|
125 | return createdDocument.body.innerHTML
|
---|
126 | }
|
---|