CVE-2023-29201 (published 2023-04-15T15:15Z)

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.

CVSS v3 base score: 9.0, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-79
cwe_name: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
extended_description: 
            Cross-site scripting (XSS) vulnerabilities occur when:
            
               Untrusted data enters a web application, typically from a web request.
               The web application dynamically generates a web page that contains this untrusted data.
               During page generation, the application does not prevent the data from containing content that is executable by a web browser, such as JavaScript, HTML tags, HTML attributes, mouse events, Flash, ActiveX, etc.
               A victim visits the generated web page through a web browser, which contains malicious script that was injected using the untrusted data.
               Since the script comes from a web page that was sent by the web server, the victim's web browser executes the malicious script in the context of the web server's domain.
               This effectively violates the intention of the web browser's same-origin policy, which states that scripts in one domain should not be able to access resources or run code in a different domain.
            
            There are three main kinds of XSS:
            
               
                  Type 1: Reflected XSS (or Non-Persistent) - 
         			The server reads data directly from the HTTP request and reflects it back in the HTTP response. Reflected XSS exploits occur when an attacker causes a victim to supply dangerous content to a vulnerable web application, which is then reflected back to the victim and executed by the web browser. The most common mechanism for delivering malicious content is to include it as a parameter in a URL that is posted publicly or e-mailed directly to the victim. URLs constructed in this manner constitute the core of many phishing schemes, whereby an attacker convinces a victim to visit a URL that refers to a vulnerable site. After the site reflects the attacker's content back to the victim, the content is executed by the victim's browser.
               
                  Type 2: Stored XSS (or Persistent) - 
               The application stores dangerous data in a database, message forum, visitor log, or other trusted data store. At a later time, the dangerous data is subsequently read back into the application and included in dynamic content. From an attacker's perspective, the optimal place to inject malicious content is in an area that is displayed to either many users or particularly interesting users. Interesting users typically have elevated privileges in the application or interact with sensitive data that is valuable to the attacker. If one of these users executes malicious content, the attacker may be able to perform privileged operations on behalf of the user or gain access to sensitive data belonging to the user. For example, the attacker might inject XSS into a log message, which might not be handled properly when an administrator views the logs.
            
               
                  Type 0: DOM-Based XSS - 
               In DOM-based XSS, the client performs the injection of XSS into the page; in the other types, the server performs the injection. DOM-based XSS generally involves server-controlled, trusted script that is sent to the client, such as Javascript that performs sanity checks on a form before the user submits it. If the server-supplied script processes user-supplied data and then injects it back into the web page (such as with dynamic HTML), then DOM-based XSS is possible.
            
            
            Once the malicious script is injected, the attacker can perform a variety of malicious activities. The attacker could transfer private information, such as cookies that may include session information, from the victim's machine to the attacker. The attacker could send malicious requests to a web site on behalf of the victim, which could be especially dangerous to the site if the victim has administrator privileges to manage that site. Phishing attacks could be used to emulate trusted web sites and trick the victim into entering a password, allowing the attacker to compromise the victim's account on that web site. Finally, the script could exploit a vulnerability in the web browser itself possibly taking over the victim's machine, sometimes referred to as "drive-by hacking."
            In many cases, the attack can be launched without the victim even being aware of it. Even with careful users, attackers frequently use a variety of methods to encode the malicious portion of the attack, such as URL encoding or Unicode, so the request looks less suspicious.
         
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-commons
commit_details: repo_url: https://github.com/xwiki/xwiki-commons
author: Michael Hamann
author_date: 2022-06-27 17:50:15+02:00
msg: 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.
repository: repo_url: https://github.com/xwiki/xwiki-commons
repo_name: xwiki/xwiki-commons
description: Common Modules used by other XWiki top level projects
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -32,7 +32,7 @@
   <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
@@ -53,6 +53,11 @@
       <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>

programming_language: HTML
filename: checkstyle-suppressions.xml
change_type: ModificationType.MODIFY
diff: @@ -36,4 +36,9 @@
   <!-- 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>

programming_language: Scala
filename: HTMLElementSanitizer.java
change_type: ModificationType.ADD
diff: @@ -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);
+}

programming_language: Java
filename: DefaultHTMLElementSanitizer.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: HTMLDefinitions.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: HTMLElementSanitizerConfiguration.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: InsecureHTMLElementSanitizer.java
change_type: ModificationType.ADD
diff: @@ -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;
+    }
+}

programming_language: Java
filename: MathMLDefinitions.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: SVGDefinitions.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: SecureHTMLElementSanitizer.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -8,5 +8,12 @@ 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

programming_language: Scala
filename: DefaultHTMLElementSanitizerTest.java
change_type: ModificationType.ADD
diff: @@ -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));
+    }
+}

programming_language: Java
filename: HTMLElementSanitizerTest.java
change_type: ModificationType.ADD
diff: @@ -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());
+    }
+}

programming_language: Java
filename: SecureHTMLElementSanitizerTest.java
change_type: ModificationType.ADD
diff: @@ -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"));
+    }
+}

programming_language: Java
repo_url: https://github.com/xwiki/xwiki-commons
commit_details: repo_url: https://github.com/xwiki/xwiki-commons
author: Michael Hamann
author_date: 2022-06-27 17:57:05+02:00
msg: XCOMMONS-1680: Filter Html attributes in restricted mode based on a whitelist

* Add a SanitizerFilter that checks namespaces and filters elements
  based on the HTMLElementSanitizer
repository: repo_url: https://github.com/xwiki/xwiki-commons
repo_name: xwiki/xwiki-commons
description: Common Modules used by other XWiki top level projects
repo_language: Java
owner: xwiki
file_changes: filename: DefaultHTMLCleaner.java
change_type: ModificationType.MODIFY
diff: @@ -107,6 +107,10 @@ public class DefaultHTMLCleaner implements HTMLCleaner
     // TODO: remove when upgrading to HTMLClener 2.23
     private HTMLFilter controlFilter;
 
+    @Inject
+    @Named("sanitizer")
+    private HTMLFilter sanitizerFilter;
+
     @Inject
     private Execution execution;
 
@@ -201,7 +205,8 @@ public HTMLCleanerConfiguration getDefaultConfiguration()
             this.listFilter,
             this.fontFilter,
             this.attributeFilter,
-            this.linkFilter));
+            this.linkFilter,
+            this.sanitizerFilter));
         return configuration;
     }
 

programming_language: Java
filename: SanitizerFilter.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,246 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+/*
+ * Alternatively, at your choice, the contents of this file may be used under the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package org.xwiki.xml.internal.html.filter;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.xml.html.HTMLCleanerConfiguration;
+import org.xwiki.xml.html.HTMLElementSanitizer;
+import org.xwiki.xml.html.filter.AbstractHTMLFilter;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+
+/**
+ * Sanitizer that sanitizes the document.
+ *
+ * @version $Id$
+ * @since 14.6RC1
+ */
+@Component
+@Named("sanitizer")
+@Singleton
+public class SanitizerFilter extends AbstractHTMLFilter
+{
+    private static final String MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
+
+    private static final String SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+
+    private static final String HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+
+    @Inject
+    private HTMLElementSanitizer htmlElementSanitizer;
+
+    @Inject
+    private SVGDefinitions svgDefinitions;
+
+    @Inject
+    private MathMLDefinitions mathMLDefinitions;
+
+    @Override
+    public void filter(Document document, Map<String, String> cleaningParameters)
+    {
+        String restricted = cleaningParameters.get(HTMLCleanerConfiguration.RESTRICTED);
+        if ("true".equalsIgnoreCase(restricted)) {
+            cleanDocument(document.getDocumentElement());
+        }
+    }
+
+    private static class TagInformation
+    {
+        public static final TagInformation INVALID = new TagInformation(null, null);
+
+        public final String tagName;
+
+        public final String namespace;
+
+        /**
+         * Default constructor.
+         *
+         * @param tagName the name of the tag
+         * @param namespace the namespace of the tag
+         */
+        TagInformation(String tagName, String namespace)
+        {
+            this.tagName = tagName;
+            this.namespace = namespace;
+        }
+    }
+
+    private void cleanDocument(Element rootElement)
+    {
+        List<Element> elementsToRemove = new ArrayList<>();
+        traverseWithNamespace(rootElement, (element, currentNamespace) -> {
+            if (currentNamespace == TagInformation.INVALID
+                || !this.htmlElementSanitizer.isElementAllowed(element.getTagName()))
+            {
+                elementsToRemove.add(element);
+                return true;
+            } else {
+                getAttributes(element).stream()
+                    .filter(
+                        attr -> !this.htmlElementSanitizer.isAttributeAllowed(element.getTagName(), attr.getName(),
+                            attr.getValue())
+                    )
+                    .forEach(element::removeAttributeNode);
+                return false;
+            }
+        });
+
+        elementsToRemove.forEach(element -> element.getParentNode().removeChild(element));
+    }
+
+    private void traverseWithNamespace(Element rootElement, BiPredicate<Element, TagInformation> traversal)
+    {
+        Node node = rootElement;
+
+        boolean reachedRoot = false;
+
+        Deque<TagInformation> parentNamespace = new ArrayDeque<>();
+        TagInformation currentNamespace = new TagInformation("html", HTML_NAMESPACE);
+        parentNamespace.push(currentNamespace);
+
+        while (!reachedRoot) {
+            boolean skipChildren = false;
+
+            if (node.getNodeType() == Node.ELEMENT_NODE && node instanceof Element) {
+                Element element = (Element) node;
+
+                currentNamespace = checkNamespace(element, parentNamespace.peek());
+                skipChildren = traversal.test(element, currentNamespace);
+            }
+
+            if (node.getFirstChild() != null && !skipChildren) {
+                node = node.getFirstChild();
+                parentNamespace.push(currentNamespace);
+            } else {
+                while (node.getNextSibling() == null) {
+                    if (node == rootElement) {
+                        reachedRoot = true;
+                        break;
+                    }
+
+                    node = node.getParentNode();
+                    currentNamespace = parentNamespace.pop();
+                }
+
+                node = node.getNextSibling();
+            }
+        }
+    }
+
+    /**
+     * Computes the namespace of the current element if it is allowed.
+     * <p>
+     * Tries to follow the logic in DOMPurify by Cure53 and other contributors | Released under the Apache license
+     * 2.0 and Mozilla Public License 2.0 - <a href="https://github.com/cure53/DOMPurify/blob/main/LICENSE">LICENSE</a>.
+     *
+     * @param element the element to check
+     * @param parentTag the information of the parent tag
+     * @return the tag information of the current tag or {@link TagInformation#INVALID} if the element must not be
+     * there
+     */
+    private TagInformation checkNamespace(Element element, TagInformation parentTag)
+    {
+        TagInformation result = TagInformation.INVALID;
+
+        // Stay in parent SVG/MathML namespace if the current element clearly belongs to the parent namespace.
+        if (SVG_NAMESPACE.equals(parentTag.namespace) && isPureSVGTag(element.getTagName(), parentTag)) {
+            result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
+        } else if (MATHML_NAMESPACE.equals(parentTag.namespace)
+            && this.mathMLDefinitions.isMathMLTag(element.getTagName()))
+        {
+            result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
+        } else if (areHTMLChildrenAllowed(parentTag)) {
+            // If HTML children are allowed, only allow the element if is actually an HTML element or the root
+            // element of MathML/SVG.
+            if ("math".equals(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), MATHML_NAMESPACE);
+            } else if ("svg".equals(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), SVG_NAMESPACE);
+            } else if (isPossiblyHtmlTag(element.getTagName())) {
+                result = new TagInformation(element.getTagName(), HTML_NAMESPACE);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @param tagName the tag name to check
+     * @param parentTag the parent information
+     * @return if the tag is an SVG tag and not also an HTML tag that is nested in an HTML integration point in SVG
+     */
+    private boolean isPureSVGTag(String tagName, TagInformation parentTag)
+    {
+        return this.svgDefinitions.isSVGTag(tagName) && (
+            !this.svgDefinitions.isHTMLIntegrationPoint(parentTag.tagName)
+                || !this.svgDefinitions.isCommonHTMLElement(tagName));
+    }
+
+    private boolean areHTMLChildrenAllowed(TagInformation parent)
+    {
+        boolean result = HTML_NAMESPACE.equals(parent.namespace);
+        result = result || (SVG_NAMESPACE.equals(parent.namespace)
+            && this.svgDefinitions.isHTMLIntegrationPoint(parent.tagName));
+        result = result || (MATHML_NAMESPACE.equals(parent.namespace)
+            && this.mathMLDefinitions.isTextOrHTMLIntegrationPoint(parent.tagName));
+        return result;
+    }
+
+    /**
+     * @param tagName the tag name to check
+     * @return if the given tag is neither a MathML tag nor an SVG tag that is also an HTML tag
+     */
+    private boolean isPossiblyHtmlTag(String tagName)
+    {
+        return !this.mathMLDefinitions.isMathMLTag(tagName)
+            && (!this.svgDefinitions.isSVGTag(tagName) || this.svgDefinitions.isCommonHTMLElement(tagName));
+    }
+
+    private List<Attr> getAttributes(Element element)
+    {
+        NamedNodeMap attributeNodes = element.getAttributes();
+        List<Attr> result = new ArrayList<>();
+
+        for (int i = 0, length = attributeNodes.getLength(); i < length; ++i) {
+            result.add((Attr) attributeNodes.item(i));
+        }
+
+        return result;
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ org.xwiki.xml.internal.html.filter.BodyFilter
 org.xwiki.xml.internal.html.filter.ControlCharactersFilter
 org.xwiki.xml.internal.html.filter.AttributeFilter
 org.xwiki.xml.internal.html.filter.UniqueIdFilter
+org.xwiki.xml.internal.html.filter.SanitizerFilter
 org.xwiki.xml.internal.html.DefaultHTMLCleaner
 org.xwiki.xml.internal.html.XWikiHTML5TagProvider
 org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer

programming_language: Scala
filename: HTMLUtilsTest.java
change_type: ModificationType.MODIFY
diff: @@ -25,11 +25,18 @@
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.context.internal.DefaultExecution;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.xml.internal.html.DefaultHTMLCleaner;
 import org.xwiki.xml.internal.html.DefaultHTMLCleanerTest;
+import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
+import org.xwiki.xml.internal.html.HTMLDefinitions;
+import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
 import org.xwiki.xml.internal.html.XWikiHTML5TagProvider;
 import org.xwiki.xml.internal.html.filter.AttributeFilter;
 import org.xwiki.xml.internal.html.filter.BodyFilter;
@@ -38,6 +45,7 @@
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -58,6 +66,14 @@
     BodyFilter.class,
     AttributeFilter.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     DefaultHTMLCleaner.class,
     DefaultExecution.class,
     XWikiHTML5TagProvider.class

programming_language: Java
filename: AbstractHTMLFilterTest.java
change_type: ModificationType.MODIFY
diff: @@ -28,11 +28,18 @@
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.context.internal.DefaultExecution;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.internal.html.DefaultHTMLCleaner;
+import org.xwiki.xml.internal.html.DefaultHTMLElementSanitizer;
+import org.xwiki.xml.internal.html.HTMLDefinitions;
+import org.xwiki.xml.internal.html.HTMLElementSanitizerConfiguration;
+import org.xwiki.xml.internal.html.MathMLDefinitions;
+import org.xwiki.xml.internal.html.SVGDefinitions;
+import org.xwiki.xml.internal.html.SecureHTMLElementSanitizer;
 import org.xwiki.xml.internal.html.XWikiHTML5TagProvider;
 import org.xwiki.xml.internal.html.filter.AttributeFilter;
 import org.xwiki.xml.internal.html.filter.BodyFilter;
@@ -41,6 +48,7 @@
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -63,6 +71,14 @@
     DefaultHTMLCleaner.class,
     DefaultExecution.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     XWikiHTML5TagProvider.class
 })
 // @formatter:on

programming_language: Java
filename: DefaultHTMLCleanerTest.java
change_type: ModificationType.MODIFY
diff: @@ -36,6 +36,7 @@
 import org.w3c.dom.Document;
 import org.w3c.dom.NodeList;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.configuration.internal.RestrictedConfigurationSourceProvider;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
@@ -49,6 +50,7 @@
 import org.xwiki.xml.internal.html.filter.LinkFilter;
 import org.xwiki.xml.internal.html.filter.ListFilter;
 import org.xwiki.xml.internal.html.filter.ListItemFilter;
+import org.xwiki.xml.internal.html.filter.SanitizerFilter;
 import org.xwiki.xml.internal.html.filter.UniqueIdFilter;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -71,6 +73,14 @@
     DefaultHTMLCleaner.class,
     LinkFilter.class,
     ControlCharactersFilter.class,
+    SanitizerFilter.class,
+    DefaultHTMLElementSanitizer.class,
+    SecureHTMLElementSanitizer.class,
+    HTMLElementSanitizerConfiguration.class,
+    RestrictedConfigurationSourceProvider.class,
+    HTMLDefinitions.class,
+    MathMLDefinitions.class,
+    SVGDefinitions.class,
     XWikiHTML5TagProvider.class
 })
 // @formatter:on
@@ -107,7 +117,7 @@ public String getHeaderFull()
 
     /**
      * Cleans using the cleaner configuration {@link DefaultHTMLCleanerTest#cleanerConfiguration}.
-     *
+     * <p>
      * Ensures that always the correct configuration is used and allows executing the same tests for HTML 4 and HTML 5.
      *
      * @param originalHtmlContent The content to clean as string.
@@ -322,6 +332,31 @@ void restrictedHtml()
         assertEquals(getHeaderFull() + "<pre>p {color:white;}</pre>" + FOOTER, result);
     }
 
+    /**
+     * Verify that the restricted parameter forbids dangerous attributes and tags.
+     */
+    @Test
+    void restrictedAttributesAndTags() throws Exception
+    {
+        Map<String, String> parameters = new HashMap<>(this.cleanerConfiguration.getParameters());
+        parameters.put("restricted", "true");
+        this.cleanerConfiguration.setParameters(parameters);
+
+        assertHTML("<p><img src=\"img.png\" /></p>", "<img onerror=\"alert(1)\" src=img.png />");
+        assertHTML("<p><a>Hello!</a></p>", "<a href=\"javascript:alert(1)\">Hello!</a>");
+        assertHTML("<p></p>", "<iframe src=\"whatever\"/>");
+
+        // Check that SVG is still working in restricted mode.
+        cleanSVGTags();
+        cleanTitleWithNamespace();
+
+        // Check that MathML is still working in restricted mode.
+        assertHTML("<p><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mtext>X</mtext><mi><span>foo</span>"
+                + "</mi></math></p>",
+            "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><span></span><mtext>X</mtext><mi><span>foo</span>"
+                + "</mi></math>");
+    }
+
     /**
      * Verify that passing a fully-formed XHTML header works fine.
      */
@@ -366,30 +401,29 @@ void cleanSVGTags() throws Exception
      * also
      * <a href="https://jira.xwiki.org/browse/XWIKI-9753">XWIKI-9753</a>).
      */
-    @Disabled("See https://jira.xwiki.org/browse/XWIKI-9753")
     @Test
     void cleanTitleWithNamespace()
     {
         // Test with TITLE in HEAD
         String input =
-            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n"
-                + "  <head>\n"
+            "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\">"
+                + "<head>\n"
                 + "    <title>Title test</title>\n"
-                + "  </head>\n"
-                + "  <body>\n"
+                + "  </head>"
+                + "<body>\n"
                 + "    <p>before</p>\n"
-                + "    <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
+                + "    <p><svg xmlns=\"http://www.w3.org/2000/svg\" height=\"300\" width=\"500\">\n"
                 + "      <g>\n"
                 + "        <title>SVG Title Demo example</title>\n"
                 + "        <rect height=\"50\" style=\"fill:none; stroke:blue; stroke-width:1px\" width=\"200\" x=\"10\" "
-                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg>\n" + "    <p>after</p>\n";
+                + "y=\"10\"></rect>\n" + "      </g>\n" + "    </svg></p>\n" + "    <p>after</p>\n";
         assertEquals(getHeader() + input + FOOTER,
             HTMLUtils.toString(clean(input)));
     }
 
     /**
-     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if {@link
-     * HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
+     * Verify that a xmlns namespace set on the HTML element is not removed by default and it's removed if
+     * {@link HTMLCleanerConfiguration#NAMESPACES_AWARE} is set to false.
      */
     @Test
     void cleanHTMLTagWithNamespace()
@@ -409,7 +443,19 @@ void cleanHTMLTagWithNamespace()
     }
 
     /**
-     * Test that cleaning an empty DIV works (it used to fail, see <a href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
+     * Check that template tags inside select don't survive, might be security-relevant, DOMPurify contains a similar
+     * check, see <a href="https://github.com/cure53/DOMPurify/commit/e32ca248c0e9450fb182e52e978631cbd78f1123">commit
+     * e32ca248c0 in DOMPurify</a>.
+     */
+    @Test
+    void cleanTemplateInsideSelect()
+    {
+        assertHTML("<p><select></select></p>", "<select><template></template></select>");
+    }
+
+    /**
+     * Test that cleaning an empty DIV works (it used to fail, see <a
+     * href="https://jira.xwiki.org/browse/XWIKI-4007">XWIKI-4007</a>).
      */
     @Test
     void cleanEmptyDIV()
@@ -607,7 +653,7 @@ void divInsideDl()
 
     /**
      * Check what happens when the dt-tag is inside div.
-     *
+     * <p>
      * This should add a wrapping dl but doesn't for HTML 4, but it works in HTML5, see
      * {@link HTML5HTMLCleanerTest#divWithDt()}.
      *

programming_language: Java