[d565449] | 1 | /**
|
---|
| 2 | * --------------------------------------------------------------------------
|
---|
| 3 | * Bootstrap util/sanitizer.js
|
---|
| 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
---|
| 5 | * --------------------------------------------------------------------------
|
---|
| 6 | */
|
---|
| 7 |
|
---|
| 8 | // js-docs-start allow-list
|
---|
| 9 | const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
|
---|
| 10 |
|
---|
| 11 | export const DefaultAllowlist = {
|
---|
| 12 | // Global attributes allowed on any supplied element below.
|
---|
| 13 | '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
|
---|
| 14 | a: ['target', 'href', 'title', 'rel'],
|
---|
| 15 | area: [],
|
---|
| 16 | b: [],
|
---|
| 17 | br: [],
|
---|
| 18 | col: [],
|
---|
| 19 | code: [],
|
---|
| 20 | dd: [],
|
---|
| 21 | div: [],
|
---|
| 22 | dl: [],
|
---|
| 23 | dt: [],
|
---|
| 24 | em: [],
|
---|
| 25 | hr: [],
|
---|
| 26 | h1: [],
|
---|
| 27 | h2: [],
|
---|
| 28 | h3: [],
|
---|
| 29 | h4: [],
|
---|
| 30 | h5: [],
|
---|
| 31 | h6: [],
|
---|
| 32 | i: [],
|
---|
| 33 | img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
|
---|
| 34 | li: [],
|
---|
| 35 | ol: [],
|
---|
| 36 | p: [],
|
---|
| 37 | pre: [],
|
---|
| 38 | s: [],
|
---|
| 39 | small: [],
|
---|
| 40 | span: [],
|
---|
| 41 | sub: [],
|
---|
| 42 | sup: [],
|
---|
| 43 | strong: [],
|
---|
| 44 | u: [],
|
---|
| 45 | ul: []
|
---|
| 46 | }
|
---|
| 47 | // js-docs-end allow-list
|
---|
| 48 |
|
---|
| 49 | const uriAttributes = new Set([
|
---|
| 50 | 'background',
|
---|
| 51 | 'cite',
|
---|
| 52 | 'href',
|
---|
| 53 | 'itemtype',
|
---|
| 54 | 'longdesc',
|
---|
| 55 | 'poster',
|
---|
| 56 | 'src',
|
---|
| 57 | 'xlink:href'
|
---|
| 58 | ])
|
---|
| 59 |
|
---|
| 60 | /**
|
---|
| 61 | * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation
|
---|
| 62 | * contexts.
|
---|
| 63 | *
|
---|
| 64 | * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
---|
| 65 | */
|
---|
| 66 | // eslint-disable-next-line unicorn/better-regex
|
---|
| 67 | const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i
|
---|
| 68 |
|
---|
| 69 | const allowedAttribute = (attribute, allowedAttributeList) => {
|
---|
| 70 | const attributeName = attribute.nodeName.toLowerCase()
|
---|
| 71 |
|
---|
| 72 | if (allowedAttributeList.includes(attributeName)) {
|
---|
| 73 | if (uriAttributes.has(attributeName)) {
|
---|
| 74 | return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))
|
---|
| 75 | }
|
---|
| 76 |
|
---|
| 77 | return true
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 | // Check if a regular expression validates the attribute.
|
---|
| 81 | return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
|
---|
| 82 | .some(regex => regex.test(attributeName))
|
---|
| 83 | }
|
---|
| 84 |
|
---|
| 85 | export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
|
---|
| 86 | if (!unsafeHtml.length) {
|
---|
| 87 | return unsafeHtml
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | if (sanitizeFunction && typeof sanitizeFunction === 'function') {
|
---|
| 91 | return sanitizeFunction(unsafeHtml)
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | const domParser = new window.DOMParser()
|
---|
| 95 | const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
|
---|
| 96 | const elements = [].concat(...createdDocument.body.querySelectorAll('*'))
|
---|
| 97 |
|
---|
| 98 | for (const element of elements) {
|
---|
| 99 | const elementName = element.nodeName.toLowerCase()
|
---|
| 100 |
|
---|
| 101 | if (!Object.keys(allowList).includes(elementName)) {
|
---|
| 102 | element.remove()
|
---|
| 103 | continue
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | const attributeList = [].concat(...element.attributes)
|
---|
| 107 | const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])
|
---|
| 108 |
|
---|
| 109 | for (const attribute of attributeList) {
|
---|
| 110 | if (!allowedAttribute(attribute, allowedAttributes)) {
|
---|
| 111 | element.removeAttribute(attribute.nodeName)
|
---|
| 112 | }
|
---|
| 113 | }
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | return createdDocument.body.innerHTML
|
---|
| 117 | }
|
---|