From dc0c709574a27070b63c100d8d87062d28be8018 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Fri, 18 Apr 2025 11:43:20 +0200 Subject: [PATCH] Ensure
  • s are wrapped in a
      or
        after sanitizing HTML This matters when displaying the versions page, where each version is in a
      • already, and developers can have release notes containing
      • of their own. --- src/amo/purify.js | 24 ++++++++++++- tests/unit/amo/utils/test_index.js | 55 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/amo/purify.js b/src/amo/purify.js index 3d721d6bccb..55d3cf0f27a 100644 --- a/src/amo/purify.js +++ b/src/amo/purify.js @@ -2,4 +2,26 @@ import createDOMPurify from 'dompurify'; import universalWindow from 'amo/window'; -export default createDOMPurify(universalWindow); +const _purify = createDOMPurify(universalWindow); +_purify.addHook('uponSanitizeElement', (node, data, config) => { + const _ALLOWED_TAGS = config.ALLOWED_TAGS || []; + if ( + node.tagName && + node.parentNode && + node.tagName === 'LI' && + !['MENU', 'OL', 'UL'].includes(node.parentNode.tagName) && + _ALLOWED_TAGS.includes('ul') && + _ALLOWED_TAGS.includes('li') + ) { + // If we find a
      • with no
          /
            / parent, create one for it. + // It's not ideal because this might create multiple independent lists for + // each item, but it's better than creating invalid HTML that would throw + // the virtual DOM off. + const oldParent = node.parentNode; + const newParent = node.ownerDocument.createElement('ul'); + newParent.appendChild(node); + oldParent.appendChild(newParent); + } +}); + +export default _purify; diff --git a/tests/unit/amo/utils/test_index.js b/tests/unit/amo/utils/test_index.js index 53d02c67d62..455db0945fb 100644 --- a/tests/unit/amo/utils/test_index.js +++ b/tests/unit/amo/utils/test_index.js @@ -698,6 +698,61 @@ describe(__filename, () => { __html: 'link', }); }); + + describe('custom `
          1. ` handling through hook', () => { + it('removes `
          2. ` if not allowed', () => { + const html = '
          3. witness me!
          4. '; + expect(sanitizeHTML(html, [])).toEqual({ + __html: 'witness me!', + }); + + expect(sanitizeHTML(html)).toEqual({ + __html: 'witness me!', + }); + }); + + it('does not wrap `
          5. ` inside a `
              ` if not allowed', () => { + const html = '
            • witness me!
            • '; + expect(sanitizeHTML(html, ['li'])).toEqual({ + __html: '
            • witness me!
            • ', + }); + }); + + it('wraps `
            • ` inside a `
                ` if they dont already', () => { + const html = '
              • witness me!
              • '; + expect(sanitizeHTML(html, ['li', 'ul'])).toEqual({ + __html: '
                • witness me!
                ', + }); + }); + + it('wraps `
              • ` inside a `
                  ` if their parent was not allowed', () => { + const html = '
                  1. witness me!
                  '; + expect(sanitizeHTML(html, ['li', 'ul'])).toEqual({ + __html: '
                  • witness me!
                  ', + }); + }); + + it('doesnt wrap `
                • ` inside a `
                    ` if not necessary', () => { + const html = '
                    • witness me!
                    '; + expect(sanitizeHTML(html, ['li', 'ul'])).toEqual({ + __html: '
                    • witness me!
                    ', + }); + }); + + it('doesnt wrap `
                  • ` inside a `
                      ` if not necessary with ol', () => { + const html = '
                      1. witness me!
                      '; + expect(sanitizeHTML(html, ['li', 'ol', 'ul'])).toEqual({ + __html: '
                      1. witness me!
                      ', + }); + }); + + it('doesnt wrap `
                    • ` inside a `
                        ` if not necessary with menu', () => { + const html = '
                      • witness me!
                      • '; + expect(sanitizeHTML(html, ['li', 'menu'])).toEqual({ + __html: '
                      • witness me!
                      • ', + }); + }); + }); }); describe('removeProtocolFromURL', () => {