CVE-2023-29201: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Commons are technical libraries common to several other top level XWiki projects. The "restricted" mode of the HTML cleaner in XWiki, introduced in version 4.2-milestone-1, only escaped `<script>` and `<style>`-tags but neither attributes that can be used to inject scripts nor other dangerous HTML tags like `<iframe>`. As a consequence, any code relying on this "restricted" mode for security is vulnerable to JavaScript injection ("cross-site scripting"/XSS). When a privileged user with programming rights visits such a comment in XWiki, the malicious JavaScript code is executed in the context of the user session. This allows server-side code execution with programming rights, impacting the confidentiality, integrity and availability of the XWiki instance. This problem has been patched in XWiki 14.6 RC1 with the introduction of a filter with allowed HTML elements and attributes that is enabled in restricted mode. There are no known workarounds apart from upgrading to a version including the fix.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 9
Published: 2023-04-15T15:15Z
Repository: https://github.com/xwiki/xwiki-commons
Language: Java
Files changed: xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/filter/SanitizerFilter.java, xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt, xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java, xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java, xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java
Commit date: 2022-06-30 12:13:38 +0200
Commit message: XCOMMONS-1680: Filter Html attributes in restricted mode based on a whitelist

* Add a SanitizerFilter that checks namespaces and filters elements
  based on the HTMLElementSanitizer

Diff: diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java
index 721ecb4b6b..10a4db0b71 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java
@@ -105,10 +105,14 @@ public class DefaultHTMLCleaner implements HTMLCleaner
     @Inject
     @Named("controlcharacters")
     // TODO: remove when upgrading to HTMLClener 2.23
     private HTMLFilter controlFilter;
 
+    @Inject
+    @Named("sanitizer")
+    private HTMLFilter sanitizerFilter;
+
     @Inject
     private Execution execution;
 
     @Inject
     private XWikiHTML5TagProvider html5TagInfoProvider;
@@ -199,11 +203,12 @@ public HTMLCleanerConfiguration getDefaultConfiguration()
             this.bodyFilter,
             this.listItemFilter,
             this.listFilter,
             this.fontFilter,
             this.attributeFilter,
-            this.linkFilter));
+            this.linkFilter,
+            this.sanitizerFilter));
         return configuration;
     }
 
     /**
      * @param configuration the configuration to use for the cleaning
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/filter/SanitizerFilter.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/filter/SanitizerFilter.java
new file mode 100644
index 0000000000..fca46b0d64
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/filter/SanitizerFilter.java
@@ -0,0 +1,246 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+/*
+ * Alternatively, at your choice, the contents of this file may be used under the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package org.xwiki.xml.internal.html.filter;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.xml.html.HTMLCleanerConfiguration;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+import org.xwiki.xml.html.filter.AbstractHTMLFilter;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+
+/**
+ * Sanitizer that sanitizes the document.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component
+@Named("sanitizer")
+@Singleton
+public class SanitizerFilter extends AbstractHTMLFilter
+{
+    private static final String MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
+
+    private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+
+    private static final String HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+
+    @Inject
+    private HTMLElementSanitizer htmlElementSanitizer;
+
+    @Inject
+    private SVGDefinitions svgDefinitions;
+
+    @Inject
+    private MathMLDefinitions mathMLDefinitions;
+
+    @Override
+    public void filter(Document document, Map<String, String> cleaningParameters)
+    {
+        String restricted = cleaningParameters.get(HTMLCleanerConfiguration.RESTRICTED);
+        if ("true".equalsIgnoreCase(restricted)) {
+            cleanDocument(document.getDocumentElement());
+        }
+    }
+
+    private static class TagInformation
+    {
+        public static final TagInformation INVALID = new TagInformation(null, null);
+
+        public final String tagName;
+
+        public final String namespace;
+
+        /**
+         * Default constructor.
+         *
+         * @param tagName the name of the tag
+         * @param namespace the namespace of the tag
+         */
+        TagInformation(String tagName, String namespace)
+        {
+            this.tagName = tagName;
+            this.namespace = namespace;
+        }
+    }
+
+    private void cleanDocument(Element rootElement)
+    {
+        List<Element> elementsToRemove = new ArrayList<>();
+        traverseWithNamespace(rootElement, (element, currentNamespace) -> {
+            if (currentNamespace == TagInformation.INVALID
+                || !this.htmlElementSanitizer.isElementAllowed(element.getTagName()))
+            {
+                elementsToRemove.add(element);
+                return true;
+            } else {
+                getAttributes(element).stream()
+                    .filter(
+                        attr -> !this.htmlElementSanitizer.isAttributeAllowed(element.getTagName(), attr.getName(),
+                            attr.getValue())
+                    )
+                    .forEach(element::removeAttributeNode);
+                return false;
+            }
+        });
+
+        elementsToRemove.forEach(element -> element.getParentNode().removeChild(element));
+    }
+
+    private void traverseWithNamespace(Element rootElement, BiPredicate<Element, TagInformation> traversal)
+    {
+        Node node = rootElement;
+
+        boolean reachedRoot = false;
+
+        Deque<TagInformation> parentNamespace = new ArrayDeque<>();
+        TagInformation currentNamespace = new TagInformation("html", HTML_NAMESPACE);
+        parentNamespace.push(currentNamespace);
+
+        while (!reachedRoot) {
+            boolean skipChildren = false;
+
+            if (node.getNodeType() == Node.ELEMENT_NODE && node instanceof Element) {
+                Element element = (Element) node;
+
+                currentNamespace = checkNamespace(element, parentNamespace.peek());
+                skipChildren = traversal.test(element, currentNamespace);
+            }
+
+            if (node.getFirstChild() != null && !skipChildren) {
+                node = node.getFirstChild();
+                parentNamespace.push(currentNamespace);
+            } else {
+                while (node.getNextSibling() == null) {
+                    if (node == rootElement) {
+                        reachedRoot = true;
+                        break;
+                    }
+
+                    node = node.getParentNode();
+                    currentNamespace = parentNamespace.pop();
+                }
+
+                node = node.getNextSibling();
+            }
+        }
+    }
+
+    /**
+     * Computes the namespace of the current element if it is allowed.
+     * <p>
+     * Tries to follow the logic in DOMPurify by Cure53 and other contributors | Released under the Apache license
+     * 2.0 and Mozilla Public License 2.0 - <a href="https://github.com/cure53/DOMPurify/blob/main/LICENSE">LICENSE</a>.
+     *
+     * @param element the element to check
+     * @param parentTag the information of the parent tag
+     * @return the tag information of the current tag or {@link TagInformation#INVALID} if the element must not be
+     * there
+     */
+    private TagInformation checkNamespace(Element element, TagInformation parentTag)
+    {
+        TagInformation result = TagInformation.INVALID;
+
+        // Stay in parent SVG/MathML namespace if the current element clearly belongs to the parent namespace.
+        if (SVG_NAMESPACE.equals(parentTag.namespace) && isPureSVGTag(element.getTagName(), parentTag)) {
+            result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
+        } else if (MATHML_NAMESPACE.equals(parentTag.namespace)
+            && this.mathMLDefinitions.isMathMLTag(element.getTagName()))
+        {
+            result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
+        } else if (areHTMLChildrenAllowed(parentTag)) {
+            // If HTML children are allowed, only allow the element if is actually an HTML element or the root
+            // element of MathML/SVG.
+            if ("math".equals(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
+            } else if ("svg".equals(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
+            } else if (isPossiblyHtmlTag(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), HTML_NAMESPACE);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @param tagName the tag name to check
+     * @param parentTag the parent information
+     * @return if the tag is an SVG tag and not also an HTML tag that is nested in an HTML integration point in SVG
+     */
+    private boolean isPureSVGTag(String tagName, TagInformation parentTag)
+    {
+        return this.svgDefinitions.isSVGTag(tagName) && (
+            !this.svgDefinitions.isHTMLIntegrationPoint(parentTag.tagName)
+                || !this.svgDefinitions.isCommonHTMLElement(tagName));
+    }
+
+    private boolean areHTMLChildrenAllowed(TagInformation parent)
+    {
+        boolean result = HTML_NAMESPACE.equals(parent.namespace);
+        result = result || (SVG_NAMESPACE.equals(parent.namespace)
+            && this.svgDefinitions.isHTMLIntegrationPoint(parent.tagName));
+        result = result || (MATHML_NAMESPACE.equals(parent.namespace)
+            && this.mathMLDefinitions.isTextOrHTMLIntegrationPoint(parent.tagName));
+        return result;
+    }
+
+    /**
+     * @param tagName the tag name to check
+     * @return if the given tag is neither a MathML tag nor an SVG tag that is also an HTML tag
+     */
+    private boolean isPossiblyHtmlTag(String tagName)
+    {
+        return !this.mathMLDefinitions.isMathMLTag(tagName)
+            && (!this.svgDefinitions.isSVGTag(tagName) || this.svgDefinitions.isCommonHTMLElement(tagName));
+    }
+
+    private List<Attr> getAttributes(Element element)
+    {
+        NamedNodeMap attributeNodes = element.getAttributes();
+        List<Attr> result = new ArrayList<>();
+
+        for (int i = 0, length = attributeNodes.getLength(); i < length; ++i) {
+            result.add((Attr) attributeNodes.item(i));
+        }
+
+        return result;
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt b/xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt
index 4d431e8491..cab5d09981 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt
@@ -4,10 +4,11 @@ org.xwiki.xml.internal.html.filter.FontFilter
 org.xwiki.xml.internal.html.filter.LinkFilter
 org.xwiki.xml.internal.html.filter.BodyFilter
 org.xwiki.xml.internal.html.filter.ControlCharactersFilter
 org.xwiki.xml.internal.html.filter.AttributeFilter
 org.xwiki.xml.internal.html.filter.UniqueIdFilter
+org.xwiki.xml.internal.html.filter.SanitizerFilter
 org.xwiki.xml.internal.html.DefaultHTMLCleaner
 org.xwiki.xml.internal.html.XWikiHTML5TagProvider
 org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer
 org.xwiki.xml.internal.html.SecureHTMLElementSanitizer
 org.xwiki.xml.internal.html.InsecureHTMLElementSanitizer
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java
index d59b56e5df..5a4322574e 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java
@@ -23,23 +23,31 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.context.internal.DefaultExecution;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.xml.internal.html.DefaultHTMLCleaner;
 import org.xwiki.xml.internal.html.DefaultHTMLCleanerTest;
+import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
+import org.xwiki.xml.internal.html.HTMLDefinitions;
+import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
 import org.xwiki.xml.internal.html.XWikiHTML5TagProvider;
 import org.xwiki.xml.internal.html.filter.AttributeFilter;
 import org.xwiki.xml.internal.html.filter.BodyFilter;
 import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;
 import org.xwiki.xml.internal.html.filter.FontFilter;
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -56,10 +64,18 @@
     ListItemFilter.class,
     FontFilter.class,
     BodyFilter.class,
     AttributeFilter.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     DefaultHTMLCleaner.class,
     DefaultExecution.class,
     XWikiHTML5TagProvider.class
 })
 class HTMLUtilsTest
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java
index 213e5c8bdc..be9172cb66 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java
@@ -26,23 +26,31 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.context.internal.DefaultExecution;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.internal.html.DefaultHTMLCleaner;
+import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
+import org.xwiki.xml.internal.html.HTMLDefinitions;
+import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
 import org.xwiki.xml.internal.html.XWikiHTML5TagProvider;
 import org.xwiki.xml.internal.html.filter.AttributeFilter;
 import org.xwiki.xml.internal.html.filter.BodyFilter;
 import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;
 import org.xwiki.xml.internal.html.filter.FontFilter;
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -61,10 +69,18 @@
     BodyFilter.class,
     AttributeFilter.class,
     DefaultHTMLCleaner.class,
     DefaultExecution.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     XWikiHTML5TagProvider.class
 })
 // @formatter:on
 class AbstractHTMLFilterTest
 {
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java
index 390decdc9e..e1c752e9a4 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java
@@ -34,10 +34,11 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.w3c.dom.NodeList;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.xml.html.HTMLCleanerConfiguration;
 import org.xwiki.xml.html.HTMLUtils;
@@ -47,10 +48,11 @@
 import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;
 import org.xwiki.xml.internal.html.filter.FontFilter;
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 import org.xwiki.xml.internal.html.filter.UniqueIdFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 /**
@@ -69,10 +71,18 @@
     AttributeFilter.class,
     UniqueIdFilter.class,
     DefaultHTMLCleaner.class,
     LinkFilter.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     XWikiHTML5TagProvider.class
 })
 // @formatter:on
 public class DefaultHTMLCleanerTest
 {
@@ -105,11 +115,11 @@ public String getHeaderFull()
         return HEADER_FULL;
     }
 
     /**
      * Cleans using the cleaner configuration {@link DefaultHTMLCleanerTest#cleanerConfiguration}.
-     *
+     * <p>
      * Ensures that always the correct configuration is used and allows executing the same tests for HTML 4 and HTML 5.
      *
      * @param originalHtmlContent The content to clean as string.
      * @return The cleaned document.
      */
@@ -320,10 +330,35 @@ void restrictedHtml()
 
         result = HTMLUtils.toString(document);
         assertEquals(getHeaderFull() + "<pre>p {color:white;}</pre>" + FOOTER, result);
     }
 
+    /**
+     * Verify that the restricted parameter forbids dangerous attributes and tags.
+     */
+    @Test
+    void restrictedAttributesAndTags() throws Exception
+    {
+        Map<String, String> parameters = new HashMap<>(this.cleanerConfiguration.getParameters());
+        parameters.put("restricted", "true");
+        this.cleanerConfiguration.setParameters(parameters);
+
+        assertHTML("<p><img src=\"img.png\" /></p>", "<img onerror=\"alert(1)\" src=img.png />");
+        assertHTML("<p><a>Hello!</a></p>", "<a href=\"javascript:alert(1)\">Hello!</a>");
+        assertHTML("<p></p>", "<iframe src=\"whatever\"/>");
+
+        // Check that SVG is still working in restricted mode.
+        cleanSVGTags();
+        cleanTitleWithNamespace();
+
+        // Check that MathML is still working in restricted mode.
+        assertHTML("<p><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mtext>X</mtext><mi><span>foo</span>"
+                + "</mi></math></p>",
+            "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><span></span><mtext>X</mtext><mi><span>foo</span>"
+                + "</mi></math>");
+    }
+
     /**
      * Verify that passing a fully-formed XHTML header works fine.
      */
     @Test
     void fullXHTMLHeader()
@@ -364,34 +399,33 @@ void cleanSVGTags() throws Exception
      * Test that cleaning works when there's a TITLE element in the body (but with a namespace). The issue was that
      * HTMLCleaner would consider it a duplicate of the TITLE element in the HEAD even though it's namespaced. (see
      * also
      * <a href="https://jira.xwiki.org/browse/XWIKI-9753">XWIKI-9753</a>).
      */
-    @Disabled("See https://jira.xwiki.org/browse/XWIKI-9753")
     @Test
     void cleanTitleWithNamespace()
     {
         // Test with TITLE in HEAD
         String input =
-            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n"
-                + "  <head>\n"
+            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\">"
+                + "<head>\n"
                 + "    <title>Title test</title>\n"
-                + "  </head>\n"
-                + "  <body>\n"
+                + "  </head>"
+                + "<body>\n"
                 + "    <p>before</p>\n"
-                + "    <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
+                + "    <p><svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
                 + "      <g>\n"
                 + "        <title>SVG Title Demo example</title>\n"
                 + "        <rect height=\"50\" style=\"fill:none; stroke:blue; stroke-width:1px\" width=\"200\" x=\"10\" "
-                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg>\n" + "    <p>after</p>\n";
+                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg></p>\n" + "    <p>after</p>\n";
         assertEquals(getHeader() + input + FOOTER,
             HTMLUtils.toString(clean(input)));
     }
 
     /**
-     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if {@link
-     * HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
+     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if
+     * {@link HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
      */
     @Test
     void cleanHTMLTagWithNamespace()
     {
         String input = "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>";
@@ -407,11 +441,23 @@ void cleanHTMLTagWithNamespace()
         assertEquals(getHeader() + "<html><head></head><body>" + FOOTER,
             HTMLUtils.toString(clean(input)));
     }
 
     /**
-     * Test that cleaning an empty DIV works (it used to fail, see <a href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
+     * Check that template tags inside select don't survive, might be security-relevant, DOMPurify contains a similar
+     * check, see <a href="https://github.com/cure53/DOMPurify/commit/e32ca248c0e9450fb182e52e978631cbd78f1123">commit
+     * e32ca248c0 in DOMPurify</a>.
+     */
+    @Test
+    void cleanTemplateInsideSelect()
+    {
+        assertHTML("<p><select></select></p>", "<select><template></template></select>");
+    }
+
+    /**
+     * Test that cleaning an empty DIV works (it used to fail, see <a
+     * href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
      */
     @Test
     void cleanEmptyDIV()
     {
         String input = "<div id=\"y\"></div><div id=\"z\">something</div>";
@@ -605,11 +651,11 @@ void divInsideDl()
             "<dl><div><dt>HTML<dd>Hypertext Markup Language</div><dt>another<dd>entry</dl>");
     }
 
     /**
      * Check what happens when the dt-tag is inside div.
-     *
+     * <p>
      * This should add a wrapping dl but doesn't for HTML 4, but it works in HTML5, see
      * {@link HTML5HTMLCleanerTest#divWithDt()}.
      *
      * @todo Replace by {@link HTML5HTMLCleanerTest#divWithDt()} if this should be fixed and this test is failing.
      */

Vulnerable code:             this.linkFilter));
     *
    @Disabled("See https://jira.xwiki.org/browse/XWIKI-9753")
            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n"
                + "  <head>\n"
                + "  </head>\n"
                + "  <body>\n"
                + "    <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg>\n" + "    <p>after</p>\n";
     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if {@link
     * HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
     * Test that cleaning an empty DIV works (it used to fail, see <a href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
     *
Fixed code:     @Inject
    @Named("sanitizer")
    private HTMLFilter sanitizerFilter;

            this.linkFilter,
            this.sanitizerFilter));
/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
/*
 * Alternatively, at your choice, the contents of this file may be used under the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package org.xwiki.xml.internal.html.filter;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xwiki.component.annotation.Component;
import org.xwiki.xml.html.HTMLCleanerConfiguration;
import org.xwiki.xml.html.HTMLElementSanitizer;
import org.xwiki.xml.html.filter.AbstractHTMLFilter;
import org.xwiki.xml.internal.html.MathMLDefinitions;
import org.xwiki.xml.internal.html.SVGDefinitions;

/**
 * Sanitizer that sanitizes the document.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component
@Named("sanitizer")
@Singleton
public class SanitizerFilter extends AbstractHTMLFilter
{
    private static final String MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";

    private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";

    private static final String HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";

    @Inject
    private HTMLElementSanitizer htmlElementSanitizer;

    @Inject
    private SVGDefinitions svgDefinitions;

    @Inject
    private MathMLDefinitions mathMLDefinitions;

    @Override
    public void filter(Document document, Map<String, String> cleaningParameters)
    {
        String restricted = cleaningParameters.get(HTMLCleanerConfiguration.RESTRICTED);
        if ("true".equalsIgnoreCase(restricted)) {
            cleanDocument(document.getDocumentElement());
        }
    }

    private static class TagInformation
    {
        public static final TagInformation INVALID = new TagInformation(null, null);

        public final String tagName;

        public final String namespace;

        /**
         * Default constructor.
         *
         * @param tagName the name of the tag
         * @param namespace the namespace of the tag
         */
        TagInformation(String tagName, String namespace)
        {
            this.tagName = tagName;
            this.namespace = namespace;
        }
    }

    private void cleanDocument(Element rootElement)
    {
        List<Element> elementsToRemove = new ArrayList<>();
        traverseWithNamespace(rootElement, (element, currentNamespace) -> {
            if (currentNamespace == TagInformation.INVALID
                || !this.htmlElementSanitizer.isElementAllowed(element.getTagName()))
            {
                elementsToRemove.add(element);
                return true;
            } else {
                getAttributes(element).stream()
                    .filter(
                        attr -> !this.htmlElementSanitizer.isAttributeAllowed(element.getTagName(), attr.getName(),
                            attr.getValue())
                    )
                    .forEach(element::removeAttributeNode);
                return false;
            }
        });

        elementsToRemove.forEach(element -> element.getParentNode().removeChild(element));
    }

    private void traverseWithNamespace(Element rootElement, BiPredicate<Element, TagInformation> traversal)
    {
        Node node = rootElement;

        boolean reachedRoot = false;

        Deque<TagInformation> parentNamespace = new ArrayDeque<>();
        TagInformation currentNamespace = new TagInformation("html", HTML_NAMESPACE);
        parentNamespace.push(currentNamespace);

        while (!reachedRoot) {
            boolean skipChildren = false;

            if (node.getNodeType() == Node.ELEMENT_NODE && node instanceof Element) {
                Element element = (Element) node;

                currentNamespace = checkNamespace(element, parentNamespace.peek());
                skipChildren = traversal.test(element, currentNamespace);
            }

            if (node.getFirstChild() != null && !skipChildren) {
                node = node.getFirstChild();
                parentNamespace.push(currentNamespace);
            } else {
                while (node.getNextSibling() == null) {
                    if (node == rootElement) {
                        reachedRoot = true;
                        break;
                    }

                    node = node.getParentNode();
                    currentNamespace = parentNamespace.pop();
                }

                node = node.getNextSibling();
            }
        }
    }

    /**
     * Computes the namespace of the current element if it is allowed.
     * <p>
     * Tries to follow the logic in DOMPurify by Cure53 and other contributors | Released under the Apache license
     * 2.0 and Mozilla Public License 2.0 - <a href="https://github.com/cure53/DOMPurify/blob/main/LICENSE">LICENSE</a>.
     *
     * @param element the element to check
     * @param parentTag the information of the parent tag
     * @return the tag information of the current tag or {@link TagInformation#INVALID} if the element must not be
     * there
     */
    private TagInformation checkNamespace(Element element, TagInformation parentTag)
    {
        TagInformation result = TagInformation.INVALID;

        // Stay in parent SVG/MathML namespace if the current element clearly belongs to the parent namespace.
        if (SVG_NAMESPACE.equals(parentTag.namespace) && isPureSVGTag(element.getTagName(), parentTag)) {
            result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
        } else if (MATHML_NAMESPACE.equals(parentTag.namespace)
            && this.mathMLDefinitions.isMathMLTag(element.getTagName()))
        {
            result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
        } else if (areHTMLChildrenAllowed(parentTag)) {
            // If HTML children are allowed, only allow the element if is actually an HTML element or the root
            // element of MathML/SVG.
            if ("math".equals(element.getTagName())) {
                result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
            } else if ("svg".equals(element.getTagName())) {
                result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
            } else if (isPossiblyHtmlTag(element.getTagName())) {
                result = new TagInformation(element.getTagName(), HTML_NAMESPACE);
            }
        }
        return result;
    }

    /**
     * @param tagName the tag name to check
     * @param parentTag the parent information
     * @return if the tag is an SVG tag and not also an HTML tag that is nested in an HTML integration point in SVG
     */
    private boolean isPureSVGTag(String tagName, TagInformation parentTag)
    {
        return this.svgDefinitions.isSVGTag(tagName) && (
            !this.svgDefinitions.isHTMLIntegrationPoint(parentTag.tagName)
                || !this.svgDefinitions.isCommonHTMLElement(tagName));
    }

    private boolean areHTMLChildrenAllowed(TagInformation parent)
    {
        boolean result = HTML_NAMESPACE.equals(parent.namespace);
        result = result || (SVG_NAMESPACE.equals(parent.namespace)
            && this.svgDefinitions.isHTMLIntegrationPoint(parent.tagName));
        result = result || (MATHML_NAMESPACE.equals(parent.namespace)
            && this.mathMLDefinitions.isTextOrHTMLIntegrationPoint(parent.tagName));
        return result;
    }

    /**
     * @param tagName the tag name to check
     * @return if the given tag is neither a MathML tag nor an SVG tag that is also an HTML tag
     */
    private boolean isPossiblyHtmlTag(String tagName)
    {
        return !this.mathMLDefinitions.isMathMLTag(tagName)
            && (!this.svgDefinitions.isSVGTag(tagName) || this.svgDefinitions.isCommonHTMLElement(tagName));
    }

    private List<Attr> getAttributes(Element element)
    {
        NamedNodeMap attributeNodes = element.getAttributes();
        List<Attr> result = new ArrayList<>();

        for (int i = 0, length = attributeNodes.getLength(); i < length; ++i) {
            result.add((Attr) attributeNodes.item(i));
        }

        return result;
    }
}
org.xwiki.xml.internal.html.filter.SanitizerFilter
import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
import org.xwiki.xml.internal.html.HTMLDefinitions;
import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
import org.xwiki.xml.internal.html.MathMLDefinitions;
import org.xwiki.xml.internal.html.SVGDefinitions;
import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
import org.xwiki.xml.internal.html.filter.SanitizerFilter;
    SanitizerFilter.class,
    DefaultHTMLElementSanitizer.class,
    SecureHTMLElementSanitizer.class,
    HTMLElementSanitizerConfiguration.class,
    RestrictedConfigurationSourceProvider.class,
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class,
import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
import org.xwiki.xml.internal.html.HTMLDefinitions;
import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
import org.xwiki.xml.internal.html.MathMLDefinitions;
import org.xwiki.xml.internal.html.SVGDefinitions;
import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
import org.xwiki.xml.internal.html.filter.SanitizerFilter;
    SanitizerFilter.class,
    DefaultHTMLElementSanitizer.class,
    SecureHTMLElementSanitizer.class,
    HTMLElementSanitizerConfiguration.class,
    RestrictedConfigurationSourceProvider.class,
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class,
import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
import org.xwiki.xml.internal.html.filter.SanitizerFilter;
    SanitizerFilter.class,
    DefaultHTMLElementSanitizer.class,
    SecureHTMLElementSanitizer.class,
    HTMLElementSanitizerConfiguration.class,
    RestrictedConfigurationSourceProvider.class,
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class,
     * <p>
    /**
     * Verify that the restricted parameter forbids dangerous attributes and tags.
     */
    @Test
    void restrictedAttributesAndTags() throws Exception
    {
        Map<String, String> parameters = new HashMap<>(this.cleanerConfiguration.getParameters());
        parameters.put("restricted", "true");
        this.cleanerConfiguration.setParameters(parameters);

        assertHTML("<p><img src=\"img.png\" /></p>", "<img onerror=\"alert(1)\" src=img.png />");
        assertHTML("<p><a>Hello!</a></p>", "<a href=\"javascript:alert(1)\">Hello!</a>");
        assertHTML("<p></p>", "<iframe src=\"whatever\"/>");

        // Check that SVG is still working in restricted mode.
        cleanSVGTags();
        cleanTitleWithNamespace();

        // Check that MathML is still working in restricted mode.
        assertHTML("<p><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mtext>X</mtext><mi><span>foo</span>"
                + "</mi></math></p>",
            "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><span></span><mtext>X</mtext><mi><span>foo</span>"
                + "</mi></math>");
    }

            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\">"
                + "<head>\n"
                + "  </head>"
                + "<body>\n"
                + "    <p><svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg></p>\n" + "    <p>after</p>\n";
     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if
     * {@link HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
     * Check that template tags inside select don't survive, might be security-relevant, DOMPurify contains a similar
     * check, see <a href="https://github.com/cure53/DOMPurify/commit/e32ca248c0e9450fb182e52e978631cbd78f1123">commit
     * e32ca248c0 in DOMPurify</a>.
     */
    @Test
    void cleanTemplateInsideSelect()
    {
        assertHTML("<p><select></select></p>", "<select><template></template></select>");
    }

    /**
     * Test that cleaning an empty DIV works (it used to fail, see <a
     * href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
     * <p>