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/pom.xml, xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/html/HTMLElementSanitizer.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizer.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLDefinitions.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLElementSanitizerConfiguration.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/InsecureHTMLElementSanitizer.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/MathMLDefinitions.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SVGDefinitions.java, xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.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/internal/html/DefaultHTMLElementSanitizerTest.java, xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/HTMLElementSanitizerTest.java, xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizerTest.java
Commit date: 2022-06-30 12:13:38 +0200
Commit message: XCOMMONS-2426: Provide a component for filtering safe HTML elements and attributes

* Add an interface and two implementations for an HTML element sanitizer.
* Let the default implementation dispatch to the different
  implementations depending on a configuration.
* Allow overriding the hint from the execution context to allow a
  context to be more permissive than another.
* Add configuration options for allowed elements/attributes
* Add tests.

Diff: diff --git a/xwiki-commons-core/xwiki-commons-xml/pom.xml b/xwiki-commons-core/xwiki-commons-xml/pom.xml
index b730660723..84a2d0af11 100644
--- a/xwiki-commons-core/xwiki-commons-xml/pom.xml
+++ b/xwiki-commons-core/xwiki-commons-xml/pom.xml
@@ -30,11 +30,11 @@
   <artifactId>xwiki-commons-xml</artifactId>
   <name>XWiki Commons - XML</name>
   <packaging>jar</packaging>
   <description>XWiki Commons - XML</description>
   <properties>
-    <xwiki.jacoco.instructionRatio>0.72</xwiki.jacoco.instructionRatio>
+    <xwiki.jacoco.instructionRatio>0.82</xwiki.jacoco.instructionRatio>
     <!-- There's a utility class with lots of features, allow it to have many dependencies;
          There's a SAX event listener, which requires complex code -->
     <checkstyle.suppressions.location>${basedir}/src/main/checkstyle/checkstyle-suppressions.xml
     </checkstyle.suppressions.location>
   </properties>
@@ -51,10 +51,15 @@
     <dependency>
       <groupId>org.xwiki.commons</groupId>
       <artifactId>xwiki-commons-context</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.commons</groupId>
+      <artifactId>xwiki-commons-configuration-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
     </dependency>
     <dependency>
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml b/xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml
index ff56696177..3f598eccc0 100644
--- a/xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml
@@ -34,6 +34,11 @@
   <suppress checks="AnonInnerLength" files="SAXEventWriter.java" />
 
   <!-- XWikiDomSerializer copied from DomSerializer -->
   <suppress checks="CyclomaticComplexity" files="XWikiDOMSerializer" />
   <suppress checks="NPathComplexity" files="XWikiDOMSerializer" />
+
+  <!-- These files have lists of strings copied from a source, making them constants would complicate updating from
+   upstream. -->
+  <suppress checks="MultipleStringLiterals"
+    files="SecureHTMLElementSanitizer.java|HTMLDefinitions.java|MathMLDefinitions.java|SVGDefinitions.java"/>
 </suppressions>
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/html/HTMLElementSanitizer.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/html/HTMLElementSanitizer.java
new file mode 100644
index 0000000000..038435d19a
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/html/HTMLElementSanitizer.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.html;
+
+import org.xwiki.component.annotation.Role;
+import org.xwiki.stability.Unstable;
+
+/**
+ * Provides methods to check if HTML elements and attributes/attribute values are considered safe.
+ * <p>
+ * This also includes SVG and MathML elements and attributes.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Role
+@Unstable
+public interface HTMLElementSanitizer
+{
+    /**
+     * The key under which a hint can be stored that will be used by the default implementation.
+     */
+    String EXECUTION_CONTEXT_HINT_KEY = "xml.html.htmlElementSanitizerHint";
+
+    /**
+     * @param elementName the name of the HTML element
+     * @return {@code true} if the given element is allowed in principle (given appropriate attributes)
+     */
+    boolean isElementAllowed(String elementName);
+
+    /**
+     * @param elementName the element for which the attributes shall be checked
+     * @param attributeName the attributes to check
+     * @param value the value of the attribute
+     * @return {@code true} if the attribute with this value is considered safe
+     */
+    boolean isAttributeAllowed(String elementName, String attributeName, String value);
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizer.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizer.java
new file mode 100644
index 0000000000..873005268e
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizer.java
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.manager.ComponentLookupException;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.phase.Initializable;
+import org.xwiki.component.phase.InitializationException;
+import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+import org.xwiki.stability.Unstable;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+
+/**
+ * Default {@link HTMLElementSanitizer} that loads the implementation chosen by the configuration.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component
+@Singleton
+@Unstable
+public class DefaultHTMLElementSanitizer implements HTMLElementSanitizer, Initializable
+{
+    private static final String CONFIGURATION_KEY = "xml.htmlElementSanitizer";
+
+    private HTMLElementSanitizer implementation;
+
+    @Inject
+    @Named("restricted")
+    private Provider<ConfigurationSource> configurationSourceProvider;
+
+    @Inject
+    private Execution execution;
+
+    @Inject
+    private Provider<ComponentManager> componentManagerProvider;
+
+    @Inject
+    private Logger logger;
+
+    @Override
+    public void initialize() throws InitializationException
+    {
+
+        ConfigurationSource configurationSource = this.configurationSourceProvider.get();
+
+        String hint;
+        if (configurationSource != null) {
+            hint = configurationSource.getProperty(CONFIGURATION_KEY, SecureHTMLElementSanitizer.HINT);
+        } else {
+            hint = SecureHTMLElementSanitizer.HINT;
+        }
+
+        try {
+            this.implementation = loadImplementationWithSecureFallback(hint);
+        } catch (ComponentLookupException ex) {
+            throw new InitializationException("Couldn't initialize the default secure HTMLElementSanitizer", ex);
+        }
+    }
+
+    private HTMLElementSanitizer loadImplementationWithSecureFallback(String hint) throws ComponentLookupException
+    {
+        ComponentManager componentManager = this.componentManagerProvider.get();
+        HTMLElementSanitizer result;
+
+        try {
+            result = componentManager.getInstance(HTMLElementSanitizer.class, hint);
+        } catch (ComponentLookupException e) {
+            this.logger.error("Couldn't load the configured HTMLElementSanitizer with hint [{}], falling back to the "
+                + "default secure implementation: {}", hint, ExceptionUtils.getRootCauseMessage(e));
+            result = componentManager.getInstance(HTMLElementSanitizer.class, SecureHTMLElementSanitizer.HINT);
+        }
+
+        return result;
+    }
+
+    private HTMLElementSanitizer getImplementation()
+    {
+        ExecutionContext context = this.execution.getContext();
+
+        HTMLElementSanitizer result = this.implementation;
+
+        if (context != null && context.hasProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY)) {
+            String hint = (String) context.getProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY);
+
+            try {
+                result = this.componentManagerProvider.get().getInstance(HTMLElementSanitizer.class, hint);
+            } catch (ComponentLookupException e) {
+                this.logger.error("Couldn't load the HTMLElementSanitizer with hint [{}] from the execution context, "
+                    + "falling back to the configured implementation: {}", hint, ExceptionUtils.getRootCauseMessage(e));
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean isElementAllowed(String elementName)
+    {
+        return getImplementation().isElementAllowed(elementName);
+    }
+
+    @Override
+    public boolean isAttributeAllowed(String elementName, String attributeName, String value)
+    {
+        return getImplementation().isAttributeAllowed(elementName, attributeName, value);
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLDefinitions.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLDefinitions.java
new file mode 100644
index 0000000000..00d29889fe
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLDefinitions.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+
+/**
+ * Provides definitions of safe HTML attributes and tags.
+ * <p>
+ * Unless otherwise noted, lists of elements and attributes are copied from 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>.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component(roles = HTMLDefinitions.class)
+@Singleton
+public class HTMLDefinitions
+{
+    /**
+     * Allowed HTML elements.
+     */
+    private final Set<String> htmlTags;
+
+    /**
+     * Allowed attributes.
+     */
+    private final Set<String> htmlAttributes;
+
+    /**
+     * Default constructor.
+     */
+    public HTMLDefinitions()
+    {
+        this.htmlTags = new HashSet<>(
+            Arrays.asList("a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo",
+                "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code",
+                "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog",
+                "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form",
+                "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input",
+                "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter",
+                "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt",
+                "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike",
+                "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot",
+                "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"));
+
+        // Attributes that are in general allowed. Note that "target" is not generally safe, but XWiki contains code
+        // that already adds the necessary attributes to make it safe both in HTMLCleaner and in XHTML rendering.
+        this.htmlAttributes = new HashSet<>(
+            Arrays.asList("accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture",
+                "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked",
+                "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords",
+                "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture",
+                "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "face", "for", "headers",
+                "height", "hidden", "high", "href", "hreflang", "id", "inputmode", "integrity", "ismap", "kind",
+                "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min",
+                "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum",
+                "pattern", "placeholder", "playsinline", "poster", "preload", "pubdate", "radiogroup", "readonly",
+                "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected",
+                "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary",
+                "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "xmlns", "slot",
+                "target"));
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is considered safe
+     */
+    public boolean isSafeTag(String tagName)
+    {
+        return this.htmlTags.contains(tagName);
+    }
+
+    /**
+     * @param attributeName the name of the attribute to check
+     * @return if the attribute is allowed
+     */
+    public boolean isAllowedAttribute(String attributeName)
+    {
+        return this.htmlAttributes.contains(attributeName);
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLElementSanitizerConfiguration.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLElementSanitizerConfiguration.java
new file mode 100644
index 0000000000..ac6aaae69d
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLElementSanitizerConfiguration.java
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.configuration.ConfigurationSource;
+
+/**
+ * Provides methods to easily access the configuration options of {@link org.xwiki.xml.html.HTMLElementSanitizer}.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component(roles = HTMLElementSanitizerConfiguration.class)
+@Singleton
+public class HTMLElementSanitizerConfiguration
+{
+    private static final String EXTRA_ALLOWED_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.extraAllowedTags";
+
+    private static final String EXTRA_ALLOWED_ATTRIBUTES_CONFIGURATION =
+        "xml.htmlElementSanitizer.extraAllowedAttributes";
+
+    private static final String EXTRA_URI_SAFE_ATTRIBUTES_CONFIGURATION =
+        "xml.htmlElementSanitizer.extraURISafeAttributes";
+
+    private static final String EXTRA_DATA_URI_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.extraDataUriTags";
+
+    private static final String FORBID_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.forbidTags";
+
+    private static final String FORBID_ATTRIBUTES_CONFIGURATION = "xml.htmlElementSanitizer.forbidAttributes";
+
+    private static final String ALLOW_UNKNOWN_PROTOCOLS_CONFIGURATION =
+        "xml.htmlElementSanitizer.allowUnknownProtocols";
+
+    private static final String ALLOWED_URI_REGEXP_CONFIGURATION = "xml.htmlElementSanitizer.allowedUriRegexp";
+
+    @Inject
+    @Named("restricted")
+    private Provider<ConfigurationSource> configurationSourceProvider;
+
+    private <T> T getValue(String key, Class<T> valueType, T defaultValue)
+    {
+        ConfigurationSource configurationSource = this.configurationSourceProvider.get();
+
+        T result;
+
+        if (configurationSource != null) {
+            result = configurationSource.getProperty(key, valueType, defaultValue);
+        } else {
+            result = defaultValue;
+        }
+
+        return result;
+    }
+
+    /**
+     * @return The list of additionally allowed tags
+     */
+    public List<String> getExtraAllowedTags()
+    {
+        return getValue(EXTRA_ALLOWED_TAGS_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return the list of additionally allowed attributes
+     */
+    public List<String> getExtraAllowedAttributes()
+    {
+        return getValue(EXTRA_ALLOWED_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return the list of additional tags that are safe for all kinds of URIs
+     */
+    public List<String> getExtraUriSafeAttributes()
+    {
+        return getValue(EXTRA_URI_SAFE_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return the list of additional tags whose attributes may have data URIs
+     */
+    public List<String> getExtraDataUriTags()
+    {
+        return getValue(EXTRA_DATA_URI_TAGS_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return the list of forbidden tags
+     */
+    public List<String> getForbidTags()
+    {
+        return getValue(FORBID_TAGS_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return the list of forbidden attributes
+     */
+    public List<String> getForbidAttributes()
+    {
+        return getValue(FORBID_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
+    }
+
+    /**
+     * @return if unknown protocols shall be allowed
+     */
+    public boolean isAllowUnknownProtocols()
+    {
+        return getValue(ALLOW_UNKNOWN_PROTOCOLS_CONFIGURATION, Boolean.class, Boolean.TRUE);
+    }
+
+    /**
+     * @return the regular expression for allowed URIs
+     */
+    public String getAllowedUriRegexp()
+    {
+        return getValue(ALLOWED_URI_REGEXP_CONFIGURATION, String.class, null);
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/InsecureHTMLElementSanitizer.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/InsecureHTMLElementSanitizer.java
new file mode 100644
index 0000000000..c7dfc7c4a4
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/InsecureHTMLElementSanitizer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.stability.Unstable;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+
+/**
+ * Implementation of {@link HTMLElementSanitizer} that allows all elements and attributes.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component
+@Singleton
+@Named("insecure")
+@Unstable
+public class InsecureHTMLElementSanitizer implements HTMLElementSanitizer
+{
+    @Override
+    public boolean isElementAllowed(String elementName)
+    {
+        return true;
+    }
+
+    @Override
+    public boolean isAttributeAllowed(String elementName, String attributeName, String value)
+    {
+        return true;
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/MathMLDefinitions.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/MathMLDefinitions.java
new file mode 100644
index 0000000000..85ad8d4edc
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/MathMLDefinitions.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+
+/**
+ * Provides MathML tag and attribute definitions with a focus on safe tags/attributes.
+ * <p>
+ * Unless otherwise noted, lists of elements and attributes are copied from 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>.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component(roles = MathMLDefinitions.class)
+@Singleton
+public class MathMLDefinitions
+{
+    private final Set<String> safeTags;
+
+    private final Set<String> allTags;
+
+    private final Set<String> allowedAttributes;
+
+    private final Set<String> textIntegrationPoints;
+
+    /**
+     * Default constructor.
+     */
+    public MathMLDefinitions()
+    {
+        this.safeTags = new HashSet<>(
+            Arrays.asList("math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr",
+                "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt",
+                "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover"));
+
+        this.allTags = new HashSet<>(
+            Arrays.asList("maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup",
+                "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"));
+
+        this.allTags.addAll(this.safeTags);
+
+        this.allowedAttributes = new HashSet<>(
+            Arrays.asList("accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines",
+                "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame",
+                "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground",
+                "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign",
+                "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel",
+                "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy",
+                "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"));
+
+        this.textIntegrationPoints = new HashSet<>(Arrays.asList("mi", "mo", "mn", "ms", "mtext", "annotation-xml"));
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is considered safe
+     */
+    public boolean isSafeTag(String tagName)
+    {
+        return this.safeTags.contains(tagName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is a MathML tag
+     */
+    public boolean isMathMLTag(String tagName)
+    {
+        return this.allTags.contains(tagName);
+    }
+
+    /**
+     * @param attributeName the name of the attribute to check
+     * @return if the attribute is allowed
+     */
+    public boolean isAllowedAttribute(String attributeName)
+    {
+        return this.allowedAttributes.contains(attributeName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is a text integration point, i.e., its children can be HTML elements
+     */
+    public boolean isTextOrHTMLIntegrationPoint(String tagName)
+    {
+        return this.textIntegrationPoints.contains(tagName);
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SVGDefinitions.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SVGDefinitions.java
new file mode 100644
index 0000000000..952cab78f7
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SVGDefinitions.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+
+/**
+ * Provides SVG tag and attribute definitions with a focus on safe tags/attributes.
+ * <p>
+ * Unless otherwise noted, lists of elements and attributes are copied from 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>.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component(roles = SVGDefinitions.class)
+@Singleton
+public class SVGDefinitions
+{
+    private final Set<String> safeTags;
+
+    private final Set<String> filterTags;
+
+    private final Set<String> allTags;
+
+    private final Set<String> allowedAttributes;
+
+    private final Set<String> commonHTMLElements;
+
+    private final Set<String> htmlIntegrationPoints;
+
+    /**
+     * Default constructor.
+     */
+    public SVGDefinitions()
+    {
+        this.allowedAttributes = new HashSet<>(
+            Arrays.asList("accent-height", "accumulate", "additive", "alignment-baseline", "ascent", "attributename",
+                "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip",
+                "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation",
+                "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy",
+                "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill",
+                "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family",
+                "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx",
+                "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href",
+                "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines",
+                "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength",
+                "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits",
+                "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name",
+                "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow",
+                "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits",
+                "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx",
+                "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering",
+                "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles",
+                "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap",
+                "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style",
+                "surfacescale", "systemlanguage", "tabindex", "targetx", "targety", "transform", "transform-origin",
+                "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode",
+                "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width",
+                "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2",
+                "xmlns", "y", "y1", "y2", "z", "zoomandpan"));
+
+        this.safeTags = new HashSet<>(
+            Arrays.asList("svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion",
+                "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "filter", "font", "g", "glyph",
+                "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path",
+                "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text",
+                "textpath", "title", "tref", "tspan", "view", "vkern"));
+
+        this.filterTags = new HashSet<>(
+            Arrays.asList("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
+                "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA", "feFuncB", "feFuncG",
+                "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset",
+                "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"));
+
+        this.allTags = new HashSet<>(
+            Arrays.asList("animate", "color-profile", "cursor", "discard", "fedropshadow", "font-face",
+                "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch",
+                "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set",
+                "solidcolor", "unknown", "use"));
+
+        this.allTags.addAll(this.filterTags);
+        this.allTags.addAll(this.safeTags);
+
+        this.commonHTMLElements = new HashSet<>(Arrays.asList("title", "style", "font", "a", "script"));
+
+        this.htmlIntegrationPoints = new HashSet<>(Arrays.asList("foreignobject", "desc", "title", "annotation-xml"));
+    }
+
+    /**
+     * @param attributeName the attribute to check
+     * @return if the attribute is allowed, i.e., considered safe
+     */
+    public boolean isAllowedAttribute(String attributeName)
+    {
+        return this.allowedAttributes.contains(attributeName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is considered safe
+     */
+    public boolean isSafeTag(String tagName)
+    {
+        return this.safeTags.contains(tagName) || isFilterTag(tagName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is an SVG filter
+     */
+    public boolean isFilterTag(String tagName)
+    {
+        return this.filterTags.contains(tagName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is an SVG tag
+     */
+    public boolean isSVGTag(String tagName)
+    {
+        return this.allTags.contains(tagName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag is both an HTML and an SVG tag
+     */
+    public boolean isCommonHTMLElement(String tagName)
+    {
+        return this.commonHTMLElements.contains(tagName);
+    }
+
+    /**
+     * @param tagName the name of the tag to check
+     * @return if the tag can contain HTML children
+     */
+    public boolean isHTMLIntegrationPoint(String tagName)
+    {
+        return this.htmlIntegrationPoints.contains(tagName);
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.java b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.java
new file mode 100644
index 0000000000..155c6d783b
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.java
@@ -0,0 +1,233 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.StringUtils;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.phase.Initializable;
+import org.xwiki.component.phase.InitializationException;
+import org.xwiki.stability.Unstable;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+
+/**
+ * Secure default implementation of {@link HTMLElementSanitizer} based on a definition of allowed elements and
+ * attributes.
+ * <p>
+ * This is heavily inspired by 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>.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component
+@Named(SecureHTMLElementSanitizer.HINT)
+@Singleton
+@Unstable
+public class SecureHTMLElementSanitizer implements HTMLElementSanitizer, Initializable
+{
+    /**
+     * The hint of this component.
+     */
+    public static final String HINT = "secure";
+
+    static final Pattern IS_SCRIPT_OR_DATA = Pattern.compile("^(?:\\w+script|data):", Pattern.CASE_INSENSITIVE);
+
+    static final Pattern ATTR_WHITESPACE =
+        Pattern.compile("[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]");
+
+    static final Pattern DATA_ATTR = Pattern.compile("^data-[\\-\\w.\\u00B7-\\uFFFF]");
+
+    static final Pattern ARIA_ATTR = Pattern.compile("^aria-[\\-\\w]+$");
+
+    static final Pattern IS_ALLOWED_URI = Pattern.compile("^(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):",
+        Pattern.CASE_INSENSITIVE);
+
+    static final Pattern IS_NO_URI = Pattern.compile("^(?:[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))",
+        Pattern.CASE_INSENSITIVE);
+
+    @Inject
+    private HTMLElementSanitizerConfiguration htmlElementSanitizerConfiguration;
+
+    @Inject
+    private HTMLDefinitions htmlDefinitions;
+
+    @Inject
+    private SVGDefinitions svgDefinitions;
+
+    @Inject
+    private MathMLDefinitions mathMLDefinitions;
+
+    /**
+     * Additionally allowed elements.
+     */
+    private final Set<String> extraAllowedTags;
+
+    /**
+     * Additionally allowed attributes.
+     */
+    private final Set<String> extraAllowedAttributes;
+
+    /**
+     * XML attributes that should be allowed.
+     */
+    private final Set<String> xmlAttributes;
+
+    /**
+     * Tags that are safe for data: URIs.
+     */
+    private final Set<String> dataUriTags;
+
+    /**
+     * Attributes safe for values like "javascript:".
+     */
+    private final Set<String> uriSafeAttributes;
+
+    private final Set<String> forbidTags;
+
+    private final Set<String> forbidAttributes;
+
+    private boolean allowUnknownProtocols;
+
+    private Pattern allowedUriPattern;
+
+    /**
+     * Default constructor.
+     */
+    public SecureHTMLElementSanitizer()
+    {
+        this.dataUriTags = new HashSet<>(Arrays.asList("audio", "video", "img", "source", "image", "track"));
+
+        this.uriSafeAttributes = new HashSet<>(
+            Arrays.asList("alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary",
+                "title", "value", "style", "xmlns"));
+
+        this.xmlAttributes =
+            new HashSet<>(Arrays.asList("xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"));
+
+        this.extraAllowedTags = new HashSet<>();
+
+        this.extraAllowedAttributes = new HashSet<>();
+
+        this.forbidTags = new HashSet<>();
+
+        this.forbidAttributes = new HashSet<>();
+
+        this.allowedUriPattern = IS_ALLOWED_URI;
+    }
+
+    @Override
+    public void initialize() throws InitializationException
+    {
+        this.extraAllowedTags.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedTags());
+        this.extraAllowedAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedAttributes());
+        this.uriSafeAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraUriSafeAttributes());
+        this.dataUriTags.addAll(this.htmlElementSanitizerConfiguration.getExtraDataUriTags());
+        this.allowUnknownProtocols = this.htmlElementSanitizerConfiguration.isAllowUnknownProtocols();
+        this.forbidTags.addAll(this.htmlElementSanitizerConfiguration.getForbidTags());
+        this.forbidAttributes.addAll(this.htmlElementSanitizerConfiguration.getForbidAttributes());
+        String configuredRegexp = this.htmlElementSanitizerConfiguration.getAllowedUriRegexp();
+        if (StringUtils.isNotBlank(configuredRegexp)) {
+            this.allowedUriPattern = Pattern.compile(configuredRegexp, Pattern.CASE_INSENSITIVE);
+        }
+    }
+
+    @Override
+    public boolean isElementAllowed(String elementName)
+    {
+        return !this.forbidTags.contains(elementName)
+            && (this.extraAllowedTags.contains(elementName) || isElementSafe(elementName));
+    }
+
+    private boolean isElementSafe(String elementName)
+    {
+        return this.htmlDefinitions.isSafeTag(elementName) || this.svgDefinitions.isSafeTag(elementName)
+            || this.mathMLDefinitions.isSafeTag(elementName);
+    }
+
+    @Override
+    public boolean isAttributeAllowed(String elementName, String attributeName, String attributeValue)
+    {
+        boolean result = false;
+
+        String lowerElement = elementName.toLowerCase();
+        String lowerAttribute = attributeName.toLowerCase();
+
+        if ((DATA_ATTR.matcher(lowerAttribute).find() || ARIA_ATTR.matcher(lowerAttribute).find())
+            && !this.forbidAttributes.contains(lowerAttribute))
+        {
+            result = true;
+        } else if (isAttributeAllowed(lowerAttribute) && !this.forbidAttributes.contains(lowerAttribute)) {
+            result = isAllowedValue(lowerElement, lowerAttribute, attributeValue);
+        }
+
+        return result;
+    }
+
+    private boolean isAllowedValue(String lowercaseElementName, String lowercaseAttributeName, String attributeValue)
+    {
+        // Break into several statements to avoid too long boolean expression.
+        boolean result = StringUtils.isBlank(attributeValue);
+        if (!result) {
+            String valueNoWhitespace = ATTR_WHITESPACE.matcher(attributeValue).replaceAll("");
+            result = this.uriSafeAttributes.contains(lowercaseAttributeName);
+            result = result || IS_NO_URI.matcher(valueNoWhitespace).find();
+            result = result || this.allowedUriPattern.matcher(valueNoWhitespace).find();
+            result = result || isAllowedDataValue(lowercaseElementName, lowercaseAttributeName, attributeValue);
+            result = result || (this.allowUnknownProtocols && !isScriptOrData(attributeValue));
+        }
+        return result;
+    }
+
+    private boolean isAttributeAllowed(String attributeName)
+    {
+        boolean result = this.extraAllowedAttributes.contains(attributeName);
+        result = result || this.htmlDefinitions.isAllowedAttribute(attributeName);
+        result = result || this.svgDefinitions.isAllowedAttribute(attributeName);
+        result = result || this.mathMLDefinitions.isAllowedAttribute(attributeName);
+        result = result || this.xmlAttributes.contains(attributeName);
+        return result;
+    }
+
+    private boolean isScriptOrData(String attributeValue)
+    {
+        return IS_SCRIPT_OR_DATA.matcher(ATTR_WHITESPACE.matcher(attributeValue).replaceAll("")).find();
+    }
+
+    private boolean isAllowedDataValue(String elementName, String attributeName, String attributeValue)
+    {
+        boolean attributeAllowsData = "src".equals(attributeName) || "xlink:href".equals(attributeName)
+            || "href".equals(attributeName);
+        return attributeAllowsData && !"script".equals(elementName) && attributeValue.startsWith("data:")
+            && this.dataUriTags.contains(elementName);
+    }
+}
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 55a88b55dc..4d431e8491 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
@@ -6,7 +6,14 @@ 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.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
+org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration
+org.xwiki.xml.internal.html.HTMLDefinitions
+org.xwiki.xml.internal.html.MathMLDefinitions
+org.xwiki.xml.internal.html.SVGDefinitions
 org.xwiki.xml.internal.LocalEntityResolver
 org.xwiki.xml.internal.DefaultXMLReaderFactory
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizerTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizerTest.java
new file mode 100644
index 0000000000..f64d4471d2
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizerTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.xwiki.component.manager.ComponentLookupException;
+import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+import org.xwiki.test.LogLevel;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.LogCaptureExtension;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.test.mockito.MockitoComponentManager;
+import org.xwiki.xml.html.HTMLConstants;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test the {@link DefaultHTMLElementSanitizer}.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@ComponentTest
+@ComponentList({ DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    InsecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class
+})
+class DefaultHTMLElementSanitizerTest
+{
+    private static final String EXPECTED_ERROR_LOADING_FOO =
+        "Couldn't load the configured HTMLElementSanitizer with hint [foo], falling back to "
+            + "the default secure implementation: ComponentLookupException: Can't find descriptor for the "
+            + "component with type [interface org.xwiki.xml.html.HTMLElementSanitizer] and hint [foo]";
+
+    private static final String EXPECTED_ERROR_LOADING_FOO_FROM_EXECUTION = "Couldn't load the HTMLElementSanitizer "
+        + "with hint [foo] from the execution context, falling back to the configured implementation: "
+        + "ComponentLookupException: Can't find descriptor for the component with type "
+        + "[interface org.xwiki.xml.html.HTMLElementSanitizer] and hint [foo]";
+
+    private static final String FOO = "foo";
+
+    private static final String INSECURE = "insecure";
+
+    @RegisterExtension
+    private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension(LogLevel.ERROR);
+
+    @MockComponent
+    @Named("restricted")
+    private ConfigurationSource configurationSource;
+
+    @MockComponent
+    private Execution execution;
+
+    @BeforeEach
+    void mockConfiguration()
+    {
+        when(this.configurationSource.getProperty(any(), eq(List.class), eq(Collections.emptyList())))
+            .thenReturn(Collections.emptyList());
+        when(this.configurationSource.getProperty(any(), eq(Boolean.class), eq(true))).thenReturn(true);
+    }
+
+    @Test
+    void secure(MockitoComponentManager componentManager) throws ComponentLookupException
+    {
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
+            .thenReturn(SecureHTMLElementSanitizer.HINT);
+        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
+        assertFalse(htmlElementSanitizer.isElementAllowed("no-such-element"));
+        assertFalse(htmlElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, "onerror", "hello"));
+        assertTrue(htmlElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "data-xwiki", "true"));
+    }
+
+    @Test
+    void insecure(MockitoComponentManager componentManager) throws ComponentLookupException
+    {
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
+            .thenReturn(INSECURE);
+        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
+        assertTrue(htmlElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
+    }
+
+    @Test
+    void fallback(MockitoComponentManager componentManager) throws ComponentLookupException
+    {
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT))).thenReturn(FOO);
+
+        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
+
+        assertEquals(EXPECTED_ERROR_LOADING_FOO, this.logCaptureExtension.getMessage(0));
+        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));
+    }
+
+    @Test
+    void throwingWhenFailure(MockitoComponentManager componentManager)
+    {
+        componentManager.unregisterComponent((Type) HTMLElementSanitizer.class, SecureHTMLElementSanitizer.HINT);
+
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT))).thenReturn(FOO);
+
+        ComponentLookupException exception = assertThrows(ComponentLookupException.class,
+            () -> componentManager.getInstance(HTMLElementSanitizer.class));
+        assertEquals("Couldn't initialize the default secure HTMLElementSanitizer",
+            exception.getCause().getMessage());
+
+        assertEquals(EXPECTED_ERROR_LOADING_FOO, this.logCaptureExtension.getMessage(0));
+    }
+
+    @Test
+    void customFromExecutionContext(MockitoComponentManager componentManager) throws ComponentLookupException
+    {
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
+            .thenReturn(SecureHTMLElementSanitizer.HINT);
+
+        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
+        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));
+
+        ExecutionContext context = new ExecutionContext();
+        context.setProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY, INSECURE);
+        when(this.execution.getContext()).thenReturn(context);
+
+        assertTrue(htmlElementSanitizer.isElementAllowed(FOO));
+    }
+
+    @Test
+    void fallBackToConfiguredWhenExecutionContextHintIsInvalid(MockitoComponentManager componentManager)
+        throws ComponentLookupException
+    {
+        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
+            .thenReturn(SecureHTMLElementSanitizer.HINT);
+
+        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
+        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));
+
+        ExecutionContext context = new ExecutionContext();
+        context.setProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY, FOO);
+        when(this.execution.getContext()).thenReturn(context);
+
+        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));
+
+        assertEquals(EXPECTED_ERROR_LOADING_FOO_FROM_EXECUTION, this.logCaptureExtension.getMessage(0));
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/HTMLElementSanitizerTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/HTMLElementSanitizerTest.java
new file mode 100644
index 0000000000..0a7bf87107
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/HTMLElementSanitizerTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+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.HTMLConstants;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+/**
+ * Test the {@link SecureHTMLElementSanitizer}.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@ComponentTest
+@ComponentList({
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class
+})
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class HTMLElementSanitizerTest
+{
+    private static final String JAVASCRIPT_ALERT = "javascript:alert(1)";
+
+    @InjectMockComponents
+    private SecureHTMLElementSanitizer secureHTMLElementSanitizer;
+
+    @InjectMockComponents
+    private InsecureHTMLElementSanitizer insecureHTMLElementSanitizer;
+
+    @Test
+    void scriptIsDisallowedInSecureSanitizer()
+    {
+        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, "onerror", "alert(1)"));
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
+            HTMLConstants.ATTRIBUTE_HREF, JAVASCRIPT_ALERT));
+    }
+
+    @ParameterizedTest
+    @MethodSource("allSanitizersProvider")
+    void dataAttributeIsAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
+    {
+        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_SPAN, "data-foo", JAVASCRIPT_ALERT));
+    }
+
+    @ParameterizedTest
+    @MethodSource("allSanitizersProvider")
+    void dataImgIsAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
+    {
+        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_IMG, HTMLConstants.ATTRIBUTE_SRC,
+            "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
+                + "  //8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="));
+    }
+
+    @ParameterizedTest
+    @MethodSource("allSanitizersProvider")
+    void linkProtocolsAreAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
+    {
+        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_A, HTMLConstants.ATTRIBUTE_HREF,
+            "https://www.xwiki.org"));
+        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_A, HTMLConstants.ATTRIBUTE_HREF,
+            "tel:+1234567890"));
+    }
+
+    @Test
+    void arbitraryAttributesAreDisallowedInSecureSanitizer()
+    {
+        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed("foo"));
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "bar", "baz"));
+    }
+
+    @Test
+    void arbitraryAttributesAreAllowedInInsecureSanitizer()
+    {
+        assertTrue(this.insecureHTMLElementSanitizer.isElementAllowed("xwiki"));
+        assertTrue(this.insecureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "hello", "world"));
+    }
+
+    private Arguments getSecureSanitizer()
+    {
+        // Use a supplier because the components haven't been injected yet when the arguments are collected.
+        return arguments(Named.<Supplier<HTMLElementSanitizer>>of("secure", () -> this.secureHTMLElementSanitizer));
+    }
+
+    private Arguments getInsecureSanitizer()
+    {
+        return arguments(Named.<Supplier<HTMLElementSanitizer>>of("insecure", () -> this.insecureHTMLElementSanitizer));
+    }
+
+    Stream<Arguments> allSanitizersProvider()
+    {
+        return Stream.of(getSecureSanitizer(), getInsecureSanitizer());
+    }
+}
diff --git a/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizerTest.java b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizerTest.java
new file mode 100644
index 0000000000..1722d0401c
--- /dev/null
+++ b/xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+package org.xwiki.xml.internal.html;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+import org.xwiki.test.annotation.BeforeComponent;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.xml.html.HTMLConstants;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the {@link SecureHTMLElementSanitizer}.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@ComponentTest
+@ComponentList({
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class
+})
+class SecureHTMLElementSanitizerTest
+{
+    private static final String ALLOWED_ATTRIBUTE = "allowed_attribute";
+
+    private static final String ONERROR = "onerror";
+
+    @MockComponent
+    private HTMLElementSanitizerConfiguration htmlElementSanitizerConfiguration;
+
+    @InjectMockComponents
+    private SecureHTMLElementSanitizer secureHTMLElementSanitizer;
+
+    @BeforeComponent
+    void setupMocks()
+    {
+        when(this.htmlElementSanitizerConfiguration.getForbidTags())
+            .thenReturn(Collections.singletonList(HTMLConstants.TAG_A));
+        when(this.htmlElementSanitizerConfiguration.getForbidAttributes())
+            .thenReturn(Collections.singletonList(HTMLConstants.ATTRIBUTE_ALT));
+        when(this.htmlElementSanitizerConfiguration.getExtraAllowedTags())
+            .thenReturn(Collections.singletonList(HTMLConstants.TAG_SCRIPT));
+        when(this.htmlElementSanitizerConfiguration.getExtraAllowedAttributes())
+            .thenReturn(Arrays.asList(ALLOWED_ATTRIBUTE, ONERROR));
+        when(this.htmlElementSanitizerConfiguration.getExtraUriSafeAttributes())
+            .thenReturn(Collections.singletonList(HTMLConstants.ATTRIBUTE_SRC));
+        when(this.htmlElementSanitizerConfiguration.getExtraDataUriTags())
+            .thenReturn(Arrays.asList(HTMLConstants.TAG_SCRIPT, HTMLConstants.TAG_NAV));
+        when(this.htmlElementSanitizerConfiguration.isAllowUnknownProtocols())
+            .thenReturn(false);
+        when(this.htmlElementSanitizerConfiguration.getAllowedUriRegexp())
+            .thenReturn("^(xwiki|https):");
+    }
+
+    @Test
+    void forbiddenTags()
+    {
+        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_A));
+    }
+
+    @Test
+    void forbiddenAttributes()
+    {
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG,
+            HTMLConstants.ATTRIBUTE_ALT, "XWiki"));
+    }
+
+    @Test
+    void extraAllowedTags()
+    {
+        assertTrue(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
+    }
+
+    @Test
+    void extraAllowedAttributes()
+    {
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, ALLOWED_ATTRIBUTE,
+            "value"));
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, ONERROR, "alert(1)"));
+    }
+
+    @Test
+    void extraUriSafeAttributes()
+    {
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG,
+            HTMLConstants.ATTRIBUTE_SRC, "javascript:alert(1)"));
+    }
+
+    @Test
+    void extraDataUriTags()
+    {
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_NAV,
+            HTMLConstants.ATTRIBUTE_HREF, "data:test"));
+        // Script cannot be enabled for data-attributes.
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SCRIPT,
+            HTMLConstants.ATTRIBUTE_HREF, "data:script"));
+    }
+
+    @Test
+    void restrictedURIs()
+    {
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
+            HTMLConstants.ATTRIBUTE_HREF, "https://www.xwiki.org"));
+        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
+            HTMLConstants.ATTRIBUTE_HREF, "xwiki:test"));
+        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
+            HTMLConstants.ATTRIBUTE_HREF, "http://example.com"));
+    }
+}

Vulnerable code:     <xwiki.jacoco.instructionRatio>0.72</xwiki.jacoco.instructionRatio>
Fixed code:     <xwiki.jacoco.instructionRatio>0.82</xwiki.jacoco.instructionRatio>
    <dependency>
      <groupId>org.xwiki.commons</groupId>
      <artifactId>xwiki-commons-configuration-api</artifactId>
      <version>${project.version}</version>
    </dependency>

  <!-- These files have lists of strings copied from a source, making them constants would complicate updating from
   upstream. -->
  <suppress checks="MultipleStringLiterals"
    files="SecureHTMLElementSanitizer.java|HTMLDefinitions.java|MathMLDefinitions.java|SVGDefinitions.java"/>
/*
 * 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.
 */
package org.xwiki.xml.html;

import org.xwiki.component.annotation.Role;
import org.xwiki.stability.Unstable;

/**
 * Provides methods to check if HTML elements and attributes/attribute values are considered safe.
 * <p>
 * This also includes SVG and MathML elements and attributes.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Role
@Unstable
public interface HTMLElementSanitizer
{
    /**
     * The key under which a hint can be stored that will be used by the default implementation.
     */
    String EXECUTION_CONTEXT_HINT_KEY = "xml.html.htmlElementSanitizerHint";

    /**
     * @param elementName the name of the HTML element
     * @return {@code true} if the given element is allowed in principle (given appropriate attributes)
     */
    boolean isElementAllowed(String elementName);

    /**
     * @param elementName the element for which the attributes shall be checked
     * @param attributeName the attributes to check
     * @param value the value of the attribute
     * @return {@code true} if the attribute with this value is considered safe
     */
    boolean isAttributeAllowed(String elementName, String attributeName, String value);
}
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

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

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.context.Execution;
import org.xwiki.context.ExecutionContext;
import org.xwiki.stability.Unstable;
import org.xwiki.xml.html.HTMLElementSanitizer;

/**
 * Default {@link HTMLElementSanitizer} that loads the implementation chosen by the configuration.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component
@Singleton
@Unstable
public class DefaultHTMLElementSanitizer implements HTMLElementSanitizer, Initializable
{
    private static final String CONFIGURATION_KEY = "xml.htmlElementSanitizer";

    private HTMLElementSanitizer implementation;

    @Inject
    @Named("restricted")
    private Provider<ConfigurationSource> configurationSourceProvider;

    @Inject
    private Execution execution;

    @Inject
    private Provider<ComponentManager> componentManagerProvider;

    @Inject
    private Logger logger;

    @Override
    public void initialize() throws InitializationException
    {

        ConfigurationSource configurationSource = this.configurationSourceProvider.get();

        String hint;
        if (configurationSource != null) {
            hint = configurationSource.getProperty(CONFIGURATION_KEY, SecureHTMLElementSanitizer.HINT);
        } else {
            hint = SecureHTMLElementSanitizer.HINT;
        }

        try {
            this.implementation = loadImplementationWithSecureFallback(hint);
        } catch (ComponentLookupException ex) {
            throw new InitializationException("Couldn't initialize the default secure HTMLElementSanitizer", ex);
        }
    }

    private HTMLElementSanitizer loadImplementationWithSecureFallback(String hint) throws ComponentLookupException
    {
        ComponentManager componentManager = this.componentManagerProvider.get();
        HTMLElementSanitizer result;

        try {
            result = componentManager.getInstance(HTMLElementSanitizer.class, hint);
        } catch (ComponentLookupException e) {
            this.logger.error("Couldn't load the configured HTMLElementSanitizer with hint [{}], falling back to the "
                + "default secure implementation: {}", hint, ExceptionUtils.getRootCauseMessage(e));
            result = componentManager.getInstance(HTMLElementSanitizer.class, SecureHTMLElementSanitizer.HINT);
        }

        return result;
    }

    private HTMLElementSanitizer getImplementation()
    {
        ExecutionContext context = this.execution.getContext();

        HTMLElementSanitizer result = this.implementation;

        if (context != null && context.hasProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY)) {
            String hint = (String) context.getProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY);

            try {
                result = this.componentManagerProvider.get().getInstance(HTMLElementSanitizer.class, hint);
            } catch (ComponentLookupException e) {
                this.logger.error("Couldn't load the HTMLElementSanitizer with hint [{}] from the execution context, "
                    + "falling back to the configured implementation: {}", hint, ExceptionUtils.getRootCauseMessage(e));
            }
        }

        return result;
    }

    @Override
    public boolean isElementAllowed(String elementName)
    {
        return getImplementation().isElementAllowed(elementName);
    }

    @Override
    public boolean isAttributeAllowed(String elementName, String attributeName, String value)
    {
        return getImplementation().isAttributeAllowed(elementName, attributeName, value);
    }
}
/*
 * 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;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;

/**
 * Provides definitions of safe HTML attributes and tags.
 * <p>
 * Unless otherwise noted, lists of elements and attributes are copied from 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>.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component(roles = HTMLDefinitions.class)
@Singleton
public class HTMLDefinitions
{
    /**
     * Allowed HTML elements.
     */
    private final Set<String> htmlTags;

    /**
     * Allowed attributes.
     */
    private final Set<String> htmlAttributes;

    /**
     * Default constructor.
     */
    public HTMLDefinitions()
    {
        this.htmlTags = new HashSet<>(
            Arrays.asList("a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo",
                "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code",
                "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog",
                "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form",
                "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input",
                "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter",
                "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt",
                "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike",
                "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot",
                "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"));

        // Attributes that are in general allowed. Note that "target" is not generally safe, but XWiki contains code
        // that already adds the necessary attributes to make it safe both in HTMLCleaner and in XHTML rendering.
        this.htmlAttributes = new HashSet<>(
            Arrays.asList("accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture",
                "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked",
                "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords",
                "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture",
                "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "face", "for", "headers",
                "height", "hidden", "high", "href", "hreflang", "id", "inputmode", "integrity", "ismap", "kind",
                "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min",
                "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum",
                "pattern", "placeholder", "playsinline", "poster", "preload", "pubdate", "radiogroup", "readonly",
                "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected",
                "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary",
                "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "xmlns", "slot",
                "target"));
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is considered safe
     */
    public boolean isSafeTag(String tagName)
    {
        return this.htmlTags.contains(tagName);
    }

    /**
     * @param attributeName the name of the attribute to check
     * @return if the attribute is allowed
     */
    public boolean isAllowedAttribute(String attributeName)
    {
        return this.htmlAttributes.contains(attributeName);
    }
}
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

import java.util.Collections;
import java.util.List;

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

import org.xwiki.component.annotation.Component;
import org.xwiki.configuration.ConfigurationSource;

/**
 * Provides methods to easily access the configuration options of {@link org.xwiki.xml.html.HTMLElementSanitizer}.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component(roles = HTMLElementSanitizerConfiguration.class)
@Singleton
public class HTMLElementSanitizerConfiguration
{
    private static final String EXTRA_ALLOWED_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.extraAllowedTags";

    private static final String EXTRA_ALLOWED_ATTRIBUTES_CONFIGURATION =
        "xml.htmlElementSanitizer.extraAllowedAttributes";

    private static final String EXTRA_URI_SAFE_ATTRIBUTES_CONFIGURATION =
        "xml.htmlElementSanitizer.extraURISafeAttributes";

    private static final String EXTRA_DATA_URI_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.extraDataUriTags";

    private static final String FORBID_TAGS_CONFIGURATION = "xml.htmlElementSanitizer.forbidTags";

    private static final String FORBID_ATTRIBUTES_CONFIGURATION = "xml.htmlElementSanitizer.forbidAttributes";

    private static final String ALLOW_UNKNOWN_PROTOCOLS_CONFIGURATION =
        "xml.htmlElementSanitizer.allowUnknownProtocols";

    private static final String ALLOWED_URI_REGEXP_CONFIGURATION = "xml.htmlElementSanitizer.allowedUriRegexp";

    @Inject
    @Named("restricted")
    private Provider<ConfigurationSource> configurationSourceProvider;

    private <T> T getValue(String key, Class<T> valueType, T defaultValue)
    {
        ConfigurationSource configurationSource = this.configurationSourceProvider.get();

        T result;

        if (configurationSource != null) {
            result = configurationSource.getProperty(key, valueType, defaultValue);
        } else {
            result = defaultValue;
        }

        return result;
    }

    /**
     * @return The list of additionally allowed tags
     */
    public List<String> getExtraAllowedTags()
    {
        return getValue(EXTRA_ALLOWED_TAGS_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return the list of additionally allowed attributes
     */
    public List<String> getExtraAllowedAttributes()
    {
        return getValue(EXTRA_ALLOWED_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return the list of additional tags that are safe for all kinds of URIs
     */
    public List<String> getExtraUriSafeAttributes()
    {
        return getValue(EXTRA_URI_SAFE_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return the list of additional tags whose attributes may have data URIs
     */
    public List<String> getExtraDataUriTags()
    {
        return getValue(EXTRA_DATA_URI_TAGS_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return the list of forbidden tags
     */
    public List<String> getForbidTags()
    {
        return getValue(FORBID_TAGS_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return the list of forbidden attributes
     */
    public List<String> getForbidAttributes()
    {
        return getValue(FORBID_ATTRIBUTES_CONFIGURATION, List.class, Collections.emptyList());
    }

    /**
     * @return if unknown protocols shall be allowed
     */
    public boolean isAllowUnknownProtocols()
    {
        return getValue(ALLOW_UNKNOWN_PROTOCOLS_CONFIGURATION, Boolean.class, Boolean.TRUE);
    }

    /**
     * @return the regular expression for allowed URIs
     */
    public String getAllowedUriRegexp()
    {
        return getValue(ALLOWED_URI_REGEXP_CONFIGURATION, String.class, null);
    }
}
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

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

import org.xwiki.component.annotation.Component;
import org.xwiki.stability.Unstable;
import org.xwiki.xml.html.HTMLElementSanitizer;

/**
 * Implementation of {@link HTMLElementSanitizer} that allows all elements and attributes.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component
@Singleton
@Named("insecure")
@Unstable
public class InsecureHTMLElementSanitizer implements HTMLElementSanitizer
{
    @Override
    public boolean isElementAllowed(String elementName)
    {
        return true;
    }

    @Override
    public boolean isAttributeAllowed(String elementName, String attributeName, String value)
    {
        return true;
    }
}
/*
 * 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;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;

/**
 * Provides MathML tag and attribute definitions with a focus on safe tags/attributes.
 * <p>
 * Unless otherwise noted, lists of elements and attributes are copied from 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>.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component(roles = MathMLDefinitions.class)
@Singleton
public class MathMLDefinitions
{
    private final Set<String> safeTags;

    private final Set<String> allTags;

    private final Set<String> allowedAttributes;

    private final Set<String> textIntegrationPoints;

    /**
     * Default constructor.
     */
    public MathMLDefinitions()
    {
        this.safeTags = new HashSet<>(
            Arrays.asList("math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr",
                "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt",
                "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover"));

        this.allTags = new HashSet<>(
            Arrays.asList("maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup",
                "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"));

        this.allTags.addAll(this.safeTags);

        this.allowedAttributes = new HashSet<>(
            Arrays.asList("accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines",
                "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame",
                "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground",
                "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign",
                "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel",
                "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy",
                "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"));

        this.textIntegrationPoints = new HashSet<>(Arrays.asList("mi", "mo", "mn", "ms", "mtext", "annotation-xml"));
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is considered safe
     */
    public boolean isSafeTag(String tagName)
    {
        return this.safeTags.contains(tagName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is a MathML tag
     */
    public boolean isMathMLTag(String tagName)
    {
        return this.allTags.contains(tagName);
    }

    /**
     * @param attributeName the name of the attribute to check
     * @return if the attribute is allowed
     */
    public boolean isAllowedAttribute(String attributeName)
    {
        return this.allowedAttributes.contains(attributeName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is a text integration point, i.e., its children can be HTML elements
     */
    public boolean isTextOrHTMLIntegrationPoint(String tagName)
    {
        return this.textIntegrationPoints.contains(tagName);
    }
}
/*
 * 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;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;

/**
 * Provides SVG tag and attribute definitions with a focus on safe tags/attributes.
 * <p>
 * Unless otherwise noted, lists of elements and attributes are copied from 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>.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component(roles = SVGDefinitions.class)
@Singleton
public class SVGDefinitions
{
    private final Set<String> safeTags;

    private final Set<String> filterTags;

    private final Set<String> allTags;

    private final Set<String> allowedAttributes;

    private final Set<String> commonHTMLElements;

    private final Set<String> htmlIntegrationPoints;

    /**
     * Default constructor.
     */
    public SVGDefinitions()
    {
        this.allowedAttributes = new HashSet<>(
            Arrays.asList("accent-height", "accumulate", "additive", "alignment-baseline", "ascent", "attributename",
                "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip",
                "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation",
                "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy",
                "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill",
                "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family",
                "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx",
                "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href",
                "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines",
                "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength",
                "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits",
                "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name",
                "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow",
                "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits",
                "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx",
                "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering",
                "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles",
                "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap",
                "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style",
                "surfacescale", "systemlanguage", "tabindex", "targetx", "targety", "transform", "transform-origin",
                "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode",
                "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width",
                "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2",
                "xmlns", "y", "y1", "y2", "z", "zoomandpan"));

        this.safeTags = new HashSet<>(
            Arrays.asList("svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion",
                "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "filter", "font", "g", "glyph",
                "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path",
                "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text",
                "textpath", "title", "tref", "tspan", "view", "vkern"));

        this.filterTags = new HashSet<>(
            Arrays.asList("feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
                "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA", "feFuncB", "feFuncG",
                "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset",
                "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"));

        this.allTags = new HashSet<>(
            Arrays.asList("animate", "color-profile", "cursor", "discard", "fedropshadow", "font-face",
                "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch",
                "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set",
                "solidcolor", "unknown", "use"));

        this.allTags.addAll(this.filterTags);
        this.allTags.addAll(this.safeTags);

        this.commonHTMLElements = new HashSet<>(Arrays.asList("title", "style", "font", "a", "script"));

        this.htmlIntegrationPoints = new HashSet<>(Arrays.asList("foreignobject", "desc", "title", "annotation-xml"));
    }

    /**
     * @param attributeName the attribute to check
     * @return if the attribute is allowed, i.e., considered safe
     */
    public boolean isAllowedAttribute(String attributeName)
    {
        return this.allowedAttributes.contains(attributeName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is considered safe
     */
    public boolean isSafeTag(String tagName)
    {
        return this.safeTags.contains(tagName) || isFilterTag(tagName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is an SVG filter
     */
    public boolean isFilterTag(String tagName)
    {
        return this.filterTags.contains(tagName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is an SVG tag
     */
    public boolean isSVGTag(String tagName)
    {
        return this.allTags.contains(tagName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag is both an HTML and an SVG tag
     */
    public boolean isCommonHTMLElement(String tagName)
    {
        return this.commonHTMLElements.contains(tagName);
    }

    /**
     * @param tagName the name of the tag to check
     * @return if the tag can contain HTML children
     */
    public boolean isHTMLIntegrationPoint(String tagName)
    {
        return this.htmlIntegrationPoints.contains(tagName);
    }
}
/*
 * 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;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

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

import org.apache.commons.lang3.StringUtils;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.stability.Unstable;
import org.xwiki.xml.html.HTMLElementSanitizer;

/**
 * Secure default implementation of {@link HTMLElementSanitizer} based on a definition of allowed elements and
 * attributes.
 * <p>
 * This is heavily inspired by 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>.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@Component
@Named(SecureHTMLElementSanitizer.HINT)
@Singleton
@Unstable
public class SecureHTMLElementSanitizer implements HTMLElementSanitizer, Initializable
{
    /**
     * The hint of this component.
     */
    public static final String HINT = "secure";

    static final Pattern IS_SCRIPT_OR_DATA = Pattern.compile("^(?:\\w+script|data):", Pattern.CASE_INSENSITIVE);

    static final Pattern ATTR_WHITESPACE =
        Pattern.compile("[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]");

    static final Pattern DATA_ATTR = Pattern.compile("^data-[\\-\\w.\\u00B7-\\uFFFF]");

    static final Pattern ARIA_ATTR = Pattern.compile("^aria-[\\-\\w]+$");

    static final Pattern IS_ALLOWED_URI = Pattern.compile("^(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):",
        Pattern.CASE_INSENSITIVE);

    static final Pattern IS_NO_URI = Pattern.compile("^(?:[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))",
        Pattern.CASE_INSENSITIVE);

    @Inject
    private HTMLElementSanitizerConfiguration htmlElementSanitizerConfiguration;

    @Inject
    private HTMLDefinitions htmlDefinitions;

    @Inject
    private SVGDefinitions svgDefinitions;

    @Inject
    private MathMLDefinitions mathMLDefinitions;

    /**
     * Additionally allowed elements.
     */
    private final Set<String> extraAllowedTags;

    /**
     * Additionally allowed attributes.
     */
    private final Set<String> extraAllowedAttributes;

    /**
     * XML attributes that should be allowed.
     */
    private final Set<String> xmlAttributes;

    /**
     * Tags that are safe for data: URIs.
     */
    private final Set<String> dataUriTags;

    /**
     * Attributes safe for values like "javascript:".
     */
    private final Set<String> uriSafeAttributes;

    private final Set<String> forbidTags;

    private final Set<String> forbidAttributes;

    private boolean allowUnknownProtocols;

    private Pattern allowedUriPattern;

    /**
     * Default constructor.
     */
    public SecureHTMLElementSanitizer()
    {
        this.dataUriTags = new HashSet<>(Arrays.asList("audio", "video", "img", "source", "image", "track"));

        this.uriSafeAttributes = new HashSet<>(
            Arrays.asList("alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary",
                "title", "value", "style", "xmlns"));

        this.xmlAttributes =
            new HashSet<>(Arrays.asList("xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"));

        this.extraAllowedTags = new HashSet<>();

        this.extraAllowedAttributes = new HashSet<>();

        this.forbidTags = new HashSet<>();

        this.forbidAttributes = new HashSet<>();

        this.allowedUriPattern = IS_ALLOWED_URI;
    }

    @Override
    public void initialize() throws InitializationException
    {
        this.extraAllowedTags.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedTags());
        this.extraAllowedAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraAllowedAttributes());
        this.uriSafeAttributes.addAll(this.htmlElementSanitizerConfiguration.getExtraUriSafeAttributes());
        this.dataUriTags.addAll(this.htmlElementSanitizerConfiguration.getExtraDataUriTags());
        this.allowUnknownProtocols = this.htmlElementSanitizerConfiguration.isAllowUnknownProtocols();
        this.forbidTags.addAll(this.htmlElementSanitizerConfiguration.getForbidTags());
        this.forbidAttributes.addAll(this.htmlElementSanitizerConfiguration.getForbidAttributes());
        String configuredRegexp = this.htmlElementSanitizerConfiguration.getAllowedUriRegexp();
        if (StringUtils.isNotBlank(configuredRegexp)) {
            this.allowedUriPattern = Pattern.compile(configuredRegexp, Pattern.CASE_INSENSITIVE);
        }
    }

    @Override
    public boolean isElementAllowed(String elementName)
    {
        return !this.forbidTags.contains(elementName)
            && (this.extraAllowedTags.contains(elementName) || isElementSafe(elementName));
    }

    private boolean isElementSafe(String elementName)
    {
        return this.htmlDefinitions.isSafeTag(elementName) || this.svgDefinitions.isSafeTag(elementName)
            || this.mathMLDefinitions.isSafeTag(elementName);
    }

    @Override
    public boolean isAttributeAllowed(String elementName, String attributeName, String attributeValue)
    {
        boolean result = false;

        String lowerElement = elementName.toLowerCase();
        String lowerAttribute = attributeName.toLowerCase();

        if ((DATA_ATTR.matcher(lowerAttribute).find() || ARIA_ATTR.matcher(lowerAttribute).find())
            && !this.forbidAttributes.contains(lowerAttribute))
        {
            result = true;
        } else if (isAttributeAllowed(lowerAttribute) && !this.forbidAttributes.contains(lowerAttribute)) {
            result = isAllowedValue(lowerElement, lowerAttribute, attributeValue);
        }

        return result;
    }

    private boolean isAllowedValue(String lowercaseElementName, String lowercaseAttributeName, String attributeValue)
    {
        // Break into several statements to avoid too long boolean expression.
        boolean result = StringUtils.isBlank(attributeValue);
        if (!result) {
            String valueNoWhitespace = ATTR_WHITESPACE.matcher(attributeValue).replaceAll("");
            result = this.uriSafeAttributes.contains(lowercaseAttributeName);
            result = result || IS_NO_URI.matcher(valueNoWhitespace).find();
            result = result || this.allowedUriPattern.matcher(valueNoWhitespace).find();
            result = result || isAllowedDataValue(lowercaseElementName, lowercaseAttributeName, attributeValue);
            result = result || (this.allowUnknownProtocols && !isScriptOrData(attributeValue));
        }
        return result;
    }

    private boolean isAttributeAllowed(String attributeName)
    {
        boolean result = this.extraAllowedAttributes.contains(attributeName);
        result = result || this.htmlDefinitions.isAllowedAttribute(attributeName);
        result = result || this.svgDefinitions.isAllowedAttribute(attributeName);
        result = result || this.mathMLDefinitions.isAllowedAttribute(attributeName);
        result = result || this.xmlAttributes.contains(attributeName);
        return result;
    }

    private boolean isScriptOrData(String attributeValue)
    {
        return IS_SCRIPT_OR_DATA.matcher(ATTR_WHITESPACE.matcher(attributeValue).replaceAll("")).find();
    }

    private boolean isAllowedDataValue(String elementName, String attributeName, String attributeValue)
    {
        boolean attributeAllowsData = "src".equals(attributeName) || "xlink:href".equals(attributeName)
            || "href".equals(attributeName);
        return attributeAllowsData && !"script".equals(elementName) && attributeValue.startsWith("data:")
            && this.dataUriTags.contains(elementName);
    }
}
org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer
org.xwiki.xml.internal.html.SecureHTMLElementSanitizer
org.xwiki.xml.internal.html.InsecureHTMLElementSanitizer
org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration
org.xwiki.xml.internal.html.HTMLDefinitions
org.xwiki.xml.internal.html.MathMLDefinitions
org.xwiki.xml.internal.html.SVGDefinitions
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;

import javax.inject.Named;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
import org.xwiki.context.Execution;
import org.xwiki.context.ExecutionContext;
import org.xwiki.test.LogLevel;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.junit5.LogCaptureExtension;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.test.mockito.MockitoComponentManager;
import org.xwiki.xml.html.HTMLConstants;
import org.xwiki.xml.html.HTMLElementSanitizer;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

/**
 * Test the {@link DefaultHTMLElementSanitizer}.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@ComponentTest
@ComponentList({ DefaultHTMLElementSanitizer.class,
    SecureHTMLElementSanitizer.class,
    InsecureHTMLElementSanitizer.class,
    HTMLElementSanitizerConfiguration.class,
    RestrictedConfigurationSourceProvider.class,
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class
})
class DefaultHTMLElementSanitizerTest
{
    private static final String EXPECTED_ERROR_LOADING_FOO =
        "Couldn't load the configured HTMLElementSanitizer with hint [foo], falling back to "
            + "the default secure implementation: ComponentLookupException: Can't find descriptor for the "
            + "component with type [interface org.xwiki.xml.html.HTMLElementSanitizer] and hint [foo]";

    private static final String EXPECTED_ERROR_LOADING_FOO_FROM_EXECUTION = "Couldn't load the HTMLElementSanitizer "
        + "with hint [foo] from the execution context, falling back to the configured implementation: "
        + "ComponentLookupException: Can't find descriptor for the component with type "
        + "[interface org.xwiki.xml.html.HTMLElementSanitizer] and hint [foo]";

    private static final String FOO = "foo";

    private static final String INSECURE = "insecure";

    @RegisterExtension
    private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension(LogLevel.ERROR);

    @MockComponent
    @Named("restricted")
    private ConfigurationSource configurationSource;

    @MockComponent
    private Execution execution;

    @BeforeEach
    void mockConfiguration()
    {
        when(this.configurationSource.getProperty(any(), eq(List.class), eq(Collections.emptyList())))
            .thenReturn(Collections.emptyList());
        when(this.configurationSource.getProperty(any(), eq(Boolean.class), eq(true))).thenReturn(true);
    }

    @Test
    void secure(MockitoComponentManager componentManager) throws ComponentLookupException
    {
        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
            .thenReturn(SecureHTMLElementSanitizer.HINT);
        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
        assertFalse(htmlElementSanitizer.isElementAllowed("no-such-element"));
        assertFalse(htmlElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, "onerror", "hello"));
        assertTrue(htmlElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "data-xwiki", "true"));
    }

    @Test
    void insecure(MockitoComponentManager componentManager) throws ComponentLookupException
    {
        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
            .thenReturn(INSECURE);
        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
        assertTrue(htmlElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
    }

    @Test
    void fallback(MockitoComponentManager componentManager) throws ComponentLookupException
    {
        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT))).thenReturn(FOO);

        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);

        assertEquals(EXPECTED_ERROR_LOADING_FOO, this.logCaptureExtension.getMessage(0));
        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));
    }

    @Test
    void throwingWhenFailure(MockitoComponentManager componentManager)
    {
        componentManager.unregisterComponent((Type) HTMLElementSanitizer.class, SecureHTMLElementSanitizer.HINT);

        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT))).thenReturn(FOO);

        ComponentLookupException exception = assertThrows(ComponentLookupException.class,
            () -> componentManager.getInstance(HTMLElementSanitizer.class));
        assertEquals("Couldn't initialize the default secure HTMLElementSanitizer",
            exception.getCause().getMessage());

        assertEquals(EXPECTED_ERROR_LOADING_FOO, this.logCaptureExtension.getMessage(0));
    }

    @Test
    void customFromExecutionContext(MockitoComponentManager componentManager) throws ComponentLookupException
    {
        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
            .thenReturn(SecureHTMLElementSanitizer.HINT);

        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));

        ExecutionContext context = new ExecutionContext();
        context.setProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY, INSECURE);
        when(this.execution.getContext()).thenReturn(context);

        assertTrue(htmlElementSanitizer.isElementAllowed(FOO));
    }

    @Test
    void fallBackToConfiguredWhenExecutionContextHintIsInvalid(MockitoComponentManager componentManager)
        throws ComponentLookupException
    {
        when(this.configurationSource.getProperty(any(), eq(SecureHTMLElementSanitizer.HINT)))
            .thenReturn(SecureHTMLElementSanitizer.HINT);

        HTMLElementSanitizer htmlElementSanitizer = componentManager.getInstance(HTMLElementSanitizer.class);
        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));

        ExecutionContext context = new ExecutionContext();
        context.setProperty(HTMLElementSanitizer.EXECUTION_CONTEXT_HINT_KEY, FOO);
        when(this.execution.getContext()).thenReturn(context);

        assertFalse(htmlElementSanitizer.isElementAllowed(FOO));

        assertEquals(EXPECTED_ERROR_LOADING_FOO_FROM_EXECUTION, this.logCaptureExtension.getMessage(0));
    }
}
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

import java.util.function.Supplier;
import java.util.stream.Stream;

import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
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.HTMLConstants;
import org.xwiki.xml.html.HTMLElementSanitizer;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;

/**
 * Test the {@link SecureHTMLElementSanitizer}.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@ComponentTest
@ComponentList({
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class
})
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class HTMLElementSanitizerTest
{
    private static final String JAVASCRIPT_ALERT = "javascript:alert(1)";

    @InjectMockComponents
    private SecureHTMLElementSanitizer secureHTMLElementSanitizer;

    @InjectMockComponents
    private InsecureHTMLElementSanitizer insecureHTMLElementSanitizer;

    @Test
    void scriptIsDisallowedInSecureSanitizer()
    {
        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, "onerror", "alert(1)"));
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
            HTMLConstants.ATTRIBUTE_HREF, JAVASCRIPT_ALERT));
    }

    @ParameterizedTest
    @MethodSource("allSanitizersProvider")
    void dataAttributeIsAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
    {
        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_SPAN, "data-foo", JAVASCRIPT_ALERT));
    }

    @ParameterizedTest
    @MethodSource("allSanitizersProvider")
    void dataImgIsAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
    {
        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_IMG, HTMLConstants.ATTRIBUTE_SRC,
            "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
                + "  //8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="));
    }

    @ParameterizedTest
    @MethodSource("allSanitizersProvider")
    void linkProtocolsAreAllowed(Supplier<HTMLElementSanitizer> htmlElementSanitizer)
    {
        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_A, HTMLConstants.ATTRIBUTE_HREF,
            "https://www.xwiki.org"));
        assertTrue(htmlElementSanitizer.get().isAttributeAllowed(HTMLConstants.TAG_A, HTMLConstants.ATTRIBUTE_HREF,
            "tel:+1234567890"));
    }

    @Test
    void arbitraryAttributesAreDisallowedInSecureSanitizer()
    {
        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed("foo"));
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "bar", "baz"));
    }

    @Test
    void arbitraryAttributesAreAllowedInInsecureSanitizer()
    {
        assertTrue(this.insecureHTMLElementSanitizer.isElementAllowed("xwiki"));
        assertTrue(this.insecureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SPAN, "hello", "world"));
    }

    private Arguments getSecureSanitizer()
    {
        // Use a supplier because the components haven't been injected yet when the arguments are collected.
        return arguments(Named.<Supplier<HTMLElementSanitizer>>of("secure", () -> this.secureHTMLElementSanitizer));
    }

    private Arguments getInsecureSanitizer()
    {
        return arguments(Named.<Supplier<HTMLElementSanitizer>>of("insecure", () -> this.insecureHTMLElementSanitizer));
    }

    Stream<Arguments> allSanitizersProvider()
    {
        return Stream.of(getSecureSanitizer(), getInsecureSanitizer());
    }
}
/*
 * 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.
 */
package org.xwiki.xml.internal.html;

import java.util.Arrays;
import java.util.Collections;

import org.junit.jupiter.api.Test;
import org.xwiki.test.annotation.BeforeComponent;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.xml.html.HTMLConstants;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

/**
 * Unit tests for the {@link SecureHTMLElementSanitizer}.
 *
 * @version $Id$
 * @since 14.6RC1
 */
@ComponentTest
@ComponentList({
    HTMLDefinitions.class,
    MathMLDefinitions.class,
    SVGDefinitions.class
})
class SecureHTMLElementSanitizerTest
{
    private static final String ALLOWED_ATTRIBUTE = "allowed_attribute";

    private static final String ONERROR = "onerror";

    @MockComponent
    private HTMLElementSanitizerConfiguration htmlElementSanitizerConfiguration;

    @InjectMockComponents
    private SecureHTMLElementSanitizer secureHTMLElementSanitizer;

    @BeforeComponent
    void setupMocks()
    {
        when(this.htmlElementSanitizerConfiguration.getForbidTags())
            .thenReturn(Collections.singletonList(HTMLConstants.TAG_A));
        when(this.htmlElementSanitizerConfiguration.getForbidAttributes())
            .thenReturn(Collections.singletonList(HTMLConstants.ATTRIBUTE_ALT));
        when(this.htmlElementSanitizerConfiguration.getExtraAllowedTags())
            .thenReturn(Collections.singletonList(HTMLConstants.TAG_SCRIPT));
        when(this.htmlElementSanitizerConfiguration.getExtraAllowedAttributes())
            .thenReturn(Arrays.asList(ALLOWED_ATTRIBUTE, ONERROR));
        when(this.htmlElementSanitizerConfiguration.getExtraUriSafeAttributes())
            .thenReturn(Collections.singletonList(HTMLConstants.ATTRIBUTE_SRC));
        when(this.htmlElementSanitizerConfiguration.getExtraDataUriTags())
            .thenReturn(Arrays.asList(HTMLConstants.TAG_SCRIPT, HTMLConstants.TAG_NAV));
        when(this.htmlElementSanitizerConfiguration.isAllowUnknownProtocols())
            .thenReturn(false);
        when(this.htmlElementSanitizerConfiguration.getAllowedUriRegexp())
            .thenReturn("^(xwiki|https):");
    }

    @Test
    void forbiddenTags()
    {
        assertFalse(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_A));
    }

    @Test
    void forbiddenAttributes()
    {
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG,
            HTMLConstants.ATTRIBUTE_ALT, "XWiki"));
    }

    @Test
    void extraAllowedTags()
    {
        assertTrue(this.secureHTMLElementSanitizer.isElementAllowed(HTMLConstants.TAG_SCRIPT));
    }

    @Test
    void extraAllowedAttributes()
    {
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, ALLOWED_ATTRIBUTE,
            "value"));
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG, ONERROR, "alert(1)"));
    }

    @Test
    void extraUriSafeAttributes()
    {
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_IMG,
            HTMLConstants.ATTRIBUTE_SRC, "javascript:alert(1)"));
    }

    @Test
    void extraDataUriTags()
    {
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_NAV,
            HTMLConstants.ATTRIBUTE_HREF, "data:test"));
        // Script cannot be enabled for data-attributes.
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_SCRIPT,
            HTMLConstants.ATTRIBUTE_HREF, "data:script"));
    }

    @Test
    void restrictedURIs()
    {
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
            HTMLConstants.ATTRIBUTE_HREF, "https://www.xwiki.org"));
        assertTrue(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
            HTMLConstants.ATTRIBUTE_HREF, "xwiki:test"));
        assertFalse(this.secureHTMLElementSanitizer.isAttributeAllowed(HTMLConstants.TAG_A,
            HTMLConstants.ATTRIBUTE_HREF, "http://example.com"));
    }
}