diff --git a/.eslintrc b/.eslintrc index a31405f5..ad718483 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,6 +2,7 @@ "root": true, "extends": "@1stg", "rules": { + "unicorn/prefer-set-has": "off", "unicorn/template-indent": "off" }, "overrides": [ diff --git a/src/index.ts b/src/index.ts index 724c8fa3..a9e56396 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,24 +3,34 @@ let domParser: DOMParser | undefined export type DocumentOrFragment = Document | DocumentFragment const isDocumentOrFragment = ( - el: DocumentOrFragment | Element, + el: ChildNode | DocumentOrFragment, ): el is DocumentOrFragment => el.nodeType === Node.DOCUMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE -export function getTagName(el: DocumentOrFragment): undefined -export function getTagName(el: Element): string -export function getTagName(el: DocumentOrFragment | Element): string | undefined -export function getTagName(el: DocumentOrFragment | Element) { - return 'tagName' in el ? el.tagName.toLowerCase() : undefined +const isElement = (el: ChildNode | DocumentOrFragment): el is Element => + el.nodeType === Node.ELEMENT_NODE + +const isComment = (el: ChildNode): el is Comment => + el.nodeType === Node.COMMENT_NODE + +function getTagName(el: Element): string +function getTagName(el: ChildNode | DocumentOrFragment): string | undefined +function getTagName(el: ChildNode | DocumentOrFragment) { + return isElement(el) ? el.tagName.toLowerCase() : undefined } /** * @see https://www.w3schools.com/tags/att_form.asp */ -export const DISALLOWED_FORM_ATTR_TAG_NAMES = +const DISALLOWED_FORM_ATTR_TAG_NAMES = 'button,fieldset,input,label,meter,object,output,select,textarea'.split(',') +const DISALLOWED_ATTR_NAMES = [ + 'autofocus', + ...'fld,formatas,src'.split(',').map(it => `data${it}`), +] + const sanitizeAttributes = (el: Element) => { const tagName = getTagName(el) const attrs = el.attributes @@ -30,9 +40,10 @@ const sanitizeAttributes = (el: Element) => { if (name === 'is') { attr.value = '' } else if ( - name === 'autofocus' || + DISALLOWED_ATTR_NAMES.includes(name) || (name === 'form' && DISALLOWED_FORM_ATTR_TAG_NAMES.includes(tagName)) || - /^on/i.test(name) || + /^(?:["'<=>`]|on)/i.test(name) || + /\s/.test(name) || /^(?:\w+script|data):/i.test(value.replaceAll(/\r?\n/g, '')) ) { el.removeAttributeNode(attr) @@ -44,9 +55,9 @@ const sanitizeAttributes = (el: Element) => { return el } -const sanitizeChildren = (el: T) => { - for (let i = 0, len = el.children.length; i < len; i++) { - const item = el.children[i] +const sanitizeChildren = (el: T) => { + for (let i = 0, len = el.childNodes.length; i < len; i++) { + const item = el.childNodes[i] const sanitized = sanitizeNode(item, getTagName(el)) if (sanitized === item) { continue @@ -64,49 +75,68 @@ const sanitizeChildren = (el: T) => { /** * @see https://developer.mozilla.org/en-US/docs/Web/MathML/Authoring#using_mathml */ -export const MathML_TAG_NAMES = new Set( +const MathML_TAG_NAMES = 'error,frac,i,multiscripts,n,o,over,padded,phantom,root,row,s,space,sqrt,style,sub,subsup,sup,table,td,text,tr,under,underover' .split(',') - .map(it => `m${it}`), -) + .map(it => `m${it}`) + +const DANGEROUS_OR_OBSOLETE_TAG_NAMES = 'event-source,listing' function sanitizeNode(el: Document): Document function sanitizeNode(el: DocumentFragment): DocumentFragment function sanitizeNode( - el: Element, + el: ChildNode, parentTagName?: string, -): Element | string | null +): ChildNode | string | null | undefined function sanitizeNode( - el: DocumentOrFragment | Element, + el: ChildNode | DocumentOrFragment, parentTagName?: string, ) { if (isDocumentOrFragment(el)) { return sanitizeChildren(el) } + if (isComment(el)) { + return + } + + if (!isElement(el)) { + return el + } + const tagName = getTagName(el) if ( - (parentTagName === 'math' && !MathML_TAG_NAMES.has(tagName)) || + (parentTagName === 'math' && !MathML_TAG_NAMES.includes(tagName)) || + DANGEROUS_OR_OBSOLETE_TAG_NAMES.includes(tagName) || // unknown HTML element el instanceof HTMLUnknownElement || // unknown SVG element - Object.getPrototypeOf(el) === SVGElement.prototype + (Object.getPrototypeOf(el) === SVGElement.prototype && + // https://github.com/jsdom/jsdom/issues/2734 + !['defs', 'filter', 'g', 'script'].includes(tagName)) ) { return el.textContent } switch (tagName) { + case 'style': { + if ((el as HTMLStyleElement).sheet?.cssRules.length) { + break + } + } + // eslint-disable-next-line no-fallthrough -- intended to remove empty style element + case 'embed': case 'iframe': case 'link': case 'meta': + case 'object': case 'parsererror': case 'script': // eslint-disable-next-line no-fallthrough -- deprecated tags case 'noembed': case 'xmp': { - el.remove() - return + return el.remove() } case 'template': { sanitizeChildren((el as HTMLTemplateElement).content) diff --git a/test/__snapshots__/dompurify.spec.ts.snap b/test/__snapshots__/dompurify.spec.ts.snap index f8d6ca24..2104792c 100644 --- a/test/__snapshots__/dompurify.spec.ts.snap +++ b/test/__snapshots__/dompurify.spec.ts.snap @@ -8,147 +8,51 @@ exports[`dompurify compatibility > 65 > 65 1`] = ` } `; -exports[`dompurify compatibility > 71 > 71 1`] = ` +exports[`dompurify compatibility > 81 > 81 1`] = ` { - "expected": "
//["'\`-->]]>]
", - "payload": "
//["'\`-->]]>]
", - "result": "
//["'\`-->]]>]
", + "expected": "
//["'\`-->]]>]
1>//["'\`-->]]>]
", + "payload": "
//["'\`-->]]>]
1>//["'\`-->]]>]
", + "result": "
//["'\`-->]]>]
1>//["'\`-->]]>]
", } `; -exports[`dompurify compatibility > 90 > 90 1`] = ` +exports[`dompurify compatibility > 83 > 83 1`] = ` { - "expected": "
//["'\`-->]]>]
", - "payload": "
//["'\`-->]]>]
", - "result": "
//["'\`-->]]>]
", + "expected": "
//["'\`-->]]>]
", + "payload": "
//["'\`-->]]>]
", + "result": "
//["'\`-->]]>]
", } `; -exports[`dompurify compatibility > 91 > 91 1`] = ` +exports[`dompurify compatibility > 95 > 95 1`] = ` { - "expected": [ - "
//["'\`-->]]>]
", - "
//["'\`-->]]>]
", - ], - "payload": "
//["'\`-->]]>]
", - "result": "
//["'\`-->]]>]
", -} -`; - -exports[`dompurify compatibility > 94 > 94 1`] = ` -{ - "expected": "
//["'\`-->]]>]
", - "payload": "
//["'\`-->]]>]
", - "result": "
//["'\`-->]]>]
", -} -`; - -exports[`dompurify compatibility > 97 > 97 1`] = ` -{ - "expected": "
XXX//["'\`-->]]>]
", - "payload": "
XXX//["'\`-->]]>]
", - "result": "
XXX//["'\`-->]]>]
", -} -`; - -exports[`dompurify compatibility > 98 > 98 1`] = ` -{ - "expected": "
", - "payload": "
", - "result": "
", -} -`; - -exports[`dompurify compatibility > 103 > 103 1`] = ` -{ - "expected": "
//["'\`-->]]>]
", - "payload": "
//["'\`-->]]>]
", - "result": "
alert(47)//["'\`-->]]>]
", + "expected": "
  • ", + "payload": "
  • ", + "result": "
    ", } `; -exports[`dompurify compatibility > 105 > 105 1`] = ` +exports[`dompurify compatibility > 100 > 100 1`] = ` { - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", + "expected": "
    X//["'\`-->]]>]
    ", + "payload": "
    X//["'\`-->]]>]
    ", + "result": "
    X//["'\`-->]]>]
    ", } `; -exports[`dompurify compatibility > 106 > 106 1`] = ` +exports[`dompurify compatibility > 111 > 111 1`] = ` { - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", + "expected": "
    ", + "payload": "
    ", + "result": "
    ", } `; -exports[`dompurify compatibility > 107 > 107 1`] = ` +exports[`dompurify compatibility > 112 > 112 1`] = ` { - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 109 > 109 1`] = ` -{ - "expected": "//["'\`-->]]>]", - "payload": "//["'\`-->]]>]
    ", - "result": "//["'\`-->]]>]", -} -`; - -exports[`dompurify compatibility > 113 > 113 1`] = ` -{ - "expected": "
    alert(57)//0//["'\`-->]]>]
    ", - "payload": "
    alert(57)//0//["'\`-->]]>]
    ", - "result": "
    alert(57)//0//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 117 > 117 1`] = ` -{ - "expected": "
    - - - -//["'\`-->]]>]
    ", - "payload": "
    - - - -//["'\`-->]]>]
    ", - "result": "
    - - - -//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 118 > 118 1`] = ` -{ - "expected": "
    // O10.10↓, OM10.0↓, GC6↓, FF - - // IE6, O10.10↓, OM10.0↓ - // IE6, O11.01↓, OM10.1↓//["'\`-->]]>]
    ", - "payload": "
    // O10.10↓, OM10.0↓, GC6↓, FF - - // IE6, O10.10↓, OM10.0↓ - // IE6, O11.01↓, OM10.1↓//["'\`-->]]>]
    ", - "result": "
    // O10.10↓, OM10.0↓, GC6↓, FF - - // IE6, O10.10↓, OM10.0↓ - // IE6, O11.01↓, OM10.1↓//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 120 > 120 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", + "expected": "
    //["'\`-->]]>]
    ", + "payload": "
    //["'\`-->]]>]
    ", + "result": "
    //["'\`-->]]>]
    ", } `; @@ -170,67 +74,11 @@ exports[`dompurify compatibility > 121 > 121 1`] = ` } `; -exports[`dompurify compatibility > 125 > 125 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 127 > 127 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 128 > 128 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 130 > 130 1`] = ` -{ - "expected": "
    &x;//["'\`-->]]>]
    ", - "payload": "
    &x;//["'\`-->]]>]
    ", - "result": "
    &x;//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 131 > 131 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 132 > 132 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 133 > 133 1`] = ` +exports[`dompurify compatibility > 134 > 134 1`] = ` { - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 136 > 136 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    //["'\`-->]]>]
    ", + "expected": "
    //["'\`-->]]>]
    ", + "payload": "
    //["'\`-->]]>]
    ", + "result": "
    //["'\`-->]]>]
    ", } `; @@ -248,67 +96,6 @@ exports[`dompurify compatibility > 139 > 139 1`] = ` } `; -exports[`dompurify compatibility > 141 > 141 1`] = ` -{ - "expected": "
    -
    - - - - -
    PRESS ENTER
    //["'\`-->]]>]
    ", - "payload": "
    -
    - - - - -
    PRESS ENTER
    //["'\`-->]]>]
    ", - "result": "
    -
    - - - - -
    PRESS ENTER
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 142 > 142 1`] = ` -{ - "expected": "
    [A] -"> -"> -"> -[B] -"> -[C] - -[D] -<% foo>//["'\`-->]]>]
    ", - "payload": "
    [A] -"> -"> -"> -[B] -"> -[C] - -[D] -<% foo>//["'\`-->]]>]
    ", - "result": "
    [A] -"> -"> -"> -[B] -"> -[C] - -[D] -<% foo>//["'\`-->]]>]
    ", -} -`; - exports[`dompurify compatibility > 146 > 146 1`] = ` { "expected": [ @@ -344,95 +131,6 @@ PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ%2BYWxlcnQoMSk8L3 } `; -exports[`dompurify compatibility > 148 > 148 1`] = ` -{ - "expected": "
    -
    - - -
    -//["'\`-->]]>]
    ", - "payload": "
    -
    - - -
    -//["'\`-->]]>]
    ", - "result": "
    -
    - - -
    -//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 151 > 151 1`] = ` -{ - "expected": [ - "
    //["'\`-->]]>]
    ", - "
    //["'\`-->]]>]
    ", - "
    //["'\`-->]]>]
    ", - "
    //["'\`-->]]>]
    ", - ], - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 154 > 154 1`] = ` -{ - "expected": [ - "
    //["'\`-->]]>]
    ", - "
    //["'\`-->]]>]
    ", - ], - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 156 > 156 1`] = ` -{ - "expected": [ - "
    -\`><img src=xx onerror=alert(108)></a> - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - "
    -\`><img src=xx onerror=alert(108)></a> - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - "
    -\`><img src=xx onerror=alert(108)></a> - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - "
    - - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - "
    - - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - ], - "payload": "
    -\`><img src=xx onerror=alert(108)></a> - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", - "result": "
    -\`><img src=xx onerror=alert(108)></a> - -\`><img src=xx onerror=alert(2)// -\`><img src=xx onerror=alert(3)////["'\`-->]]>]
    ", -} -`; - exports[`dompurify compatibility > 157 > 157 1`] = ` { "expected": [ @@ -493,14 +191,36 @@ exports[`dompurify compatibility > 158 > 158 1`] = ` } `; -exports[`dompurify compatibility > 161 > 161 1`] = ` +exports[`dompurify compatibility > 160 > 160 1`] = ` { - "expected": "
    XXX//["'\`-->]]>]
    + "expected": [ + "
    X
    //["'\`-->]]>]
    XXX
    +//["'\`-->]]>]
    ", + "
    X
    //["'\`-->]]>]
    XXX
    +//["'\`-->]]>]
    ", + ], + "payload": "
    X
    //["'\`-->]]>]
    XXX
    +//["'\`-->]]>]
    ", + "result": "
    X
    //["'\`-->]]>]
    XXX
    //["'\`-->]]>]
    ", - "payload": "
    XXX//["'\`-->]]>]
    -//["'\`-->]]>]
    ", - "result": "
    XXX//["'\`-->]]>]
    -//["'\`-->]]>]
    ", } `; @@ -522,18 +242,7 @@ exports[`dompurify compatibility > 172 > 172 1`] = ` "
    ]>//["'\`-->]]>]
    ", ], "payload": "
    ]> //["'\`-->]]>]
    ", - "result": "
    ]> //["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > 173 > 173 1`] = ` -{ - "expected": "
    -//["'\`-->]]>]
    ", - "payload": "
    -//["'\`-->]]>]
    ", - "result": "
    -//["'\`-->]]>]
    ", + "result": "
    ]> //["'\`-->]]>]
    ", } `; @@ -568,7 +277,7 @@ exports[`dompurify compatibility > 175 > 175 1`] = ` "
    ", ], "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", + "result": "
    //["'\`-->]]>]
    ", } `; @@ -624,7 +333,7 @@ exports[`dompurify compatibility > 178 > 178 1`] = `
    - + @@ -635,14 +344,6 @@ exports[`dompurify compatibility > 178 > 178 1`] = ` } `; -exports[`dompurify compatibility > 179 > 179 1`] = ` -{ - "expected": "
    //["'\`-->]]>]
    ", - "payload": "
    //["'\`-->]]>]
    ", - "result": "
    //["'\`-->]]>]
    ", -} -`; - exports[`dompurify compatibility > 182 > 182 1`] = ` { "expected": [ @@ -678,7 +379,7 @@ exports[`dompurify compatibility > Avoid over-zealous stripping of SVG filter el { "expected": "", "payload": "", - "result": "", + "result": "", } `; @@ -817,7 +518,7 @@ exports[`dompurify compatibility > Don't remove ARIA attributes if not prohibite { "expected": "", "payload": "", - "result": "", + "result": "", } `; @@ -856,22 +557,6 @@ exports[`dompurify compatibility > Fixed an exception coming from missing clobbe } `; -exports[`dompurify compatibility > Image after self-closing style to trick jQuery tag-completion > Image after self-closing style to trick jQuery tag-completion 1`] = ` -{ - "expected": "", - "payload": "", - "result": "", -} -`; - -exports[`dompurify compatibility > Image after style to trick jQuery tag-completion > Image after style to trick jQuery tag-completion 1`] = ` -{ - "expected": "", - "payload": "", -} -`; - exports[`dompurify compatibility > Image with data URI src > Image with data URI src 1`] = ` { "expected": "", @@ -906,15 +591,15 @@ exports[`dompurify compatibility > Img element inside noscript terminated inside { "expected": "", "payload": "", - "result": "", + "result": "", } `; -exports[`dompurify compatibility > Img inside style inside broken option element > Img inside style inside broken option element 1`] = ` +exports[`dompurify compatibility > Img element inside shadow DOM template > Img element inside shadow DOM template 1`] = ` { - "expected": "", - "payload": "", - "result": "", + "expected": "", + "payload": "", + "result": "", } `; @@ -1004,10 +689,10 @@ CLICKMEhttp://http://google.com //["'\`-->]]>]
    ", "result": "
    CLICKME - + CLICKME - + CLICKMEhttp://http://google.com //["'\`-->]]>]
    ", } @@ -1061,49 +746,6 @@ exports[`dompurify compatibility > MathML example > MathML example 1`] = ` `; exports[`dompurify compatibility > SVG > SVG 1`] = ` -{ - "expected": [ - "
    - - -//["'\`-->]]>]
    ", - "
    - - -//["'\`-->]]>]
    ", - "
    - - -//["'\`-->]]>]
    ", - "
    - - -//["'\`-->]]>]
    ", - ], - "payload": "
    - - -//["'\`-->]]>]
    ", - "result": "
    - - -//["'\`-->]]>]
    ", -} -`; - -exports[`dompurify compatibility > SVG > SVG 2`] = ` { "expected": "
    @@ -1147,7 +789,7 @@ exports[`dompurify compatibility > Tests against attribute-based mXSS behavior 1 "

    ">

    ", ], "payload": "

    ">", - "result": "

    ">

    ", + "result": "

    ">

    ", } `; @@ -1191,30 +833,6 @@ exports[`dompurify compatibility > Tests against nesting-based mXSS behavior 3/5 } `; -exports[`dompurify compatibility > Textarea and comments enabling img element > Textarea and comments enabling img element 1`] = ` -{ - "expected": "", - "payload": "", - "result": "", -} -`; - -exports[`dompurify compatibility > mXSS Variation I > mXSS Variation I 1`] = ` -{ - "expected": "<img onerror="alert(1);//" src=x>", - "payload": "<img onerror="alert(1);//" src=x>", - "result": "<img onerror="alert(1);//" src=x>", -} -`; - -exports[`dompurify compatibility > mXSS Variation II > mXSS Variation II 1`] = ` -{ - "expected": "", - "payload": "", - "result": "", -} -`; - exports[`dompurify compatibility > onclick, onsubmit, onfocus; DOM clobbering: parentNode > onclick, onsubmit, onfocus; DOM clobbering: parentNode 1`] = ` { "expected": "
    123
    ", @@ -1291,6 +909,6 @@ exports[`dompurify compatibility > src Attributes for IMG, AUDIO, VIDEO and SOUR { "expected": "
    ", "payload": "
    ", - "result": "
    ", + "result": "
    ", } `; diff --git a/test/__snapshots__/fixtures.spec.ts.snap b/test/__snapshots__/fixtures.spec.ts.snap index f1a03dec..1d0ee512 100644 --- a/test/__snapshots__/fixtures.spec.ts.snap +++ b/test/__snapshots__/fixtures.spec.ts.snap @@ -6,6 +6,14 @@ exports[`fixtures > fragment 1`] = ` `; exports[`fixtures > fragment 2`] = ` +"
    + + //["'\\\`-->]]>] +
    +" +`; + +exports[`fixtures > fragment 3`] = ` "