VYPR
Critical severityNVD Advisory· Published Apr 15, 2023· Updated Feb 6, 2025

org.xwiki.commons:xwiki-commons-xml Cross-site Scripting vulnerability

CVE-2023-29201

Description

XWiki Commons HTML cleaner restricted mode fails to filter dangerous tags/attributes, allowing stored XSS leading to RCE for privileged users.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

XWiki Commons HTML cleaner restricted mode fails to filter dangerous tags/attributes, allowing stored XSS leading to RCE for privileged users.

The XWiki Commons HTML cleaner's "restricted" mode, intended to strip unsafe HTML from user-contributed content, only escaped ` and tags, neglecting dangerous attributes like onclick and other elements such as [1]. This oversight means that any application relying on this mode for security (e.g., comments processed by the {{html}}` macro) is vulnerable to stored cross-site scripting (XSS) [4].

Exploitation

An attacker can inject malicious HTML or JavaScript via attributes (e.g., ``) or dangerous elements [4]. When a privileged user with programming rights views the crafted content, the script executes in their session, bypassing the restricted-mode sanitization [1]. No authentication is required for the attacker to place the payload, but the XSS triggers only when a privileged user accesses the compromised page.

Impact

Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of a privileged user's session. Because programming rights in XWiki enable server-side operations, the attacker can escalate to remote code execution (RCE) on the XWiki instance, compromising its confidentiality, integrity, and availability [1].

Mitigation

The vulnerability is patched in XWiki 14.6 RC1, which introduces a filter that defines a whitelist of safe HTML elements and attributes, enabled in restricted mode [2][3]. No workarounds exist; upgrading to a fixed version is required [1].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.commons:xwiki-commons-xmlMaven
>= 4.2-milestone-1, < 14.6-rc-114.6-rc-1

Affected products

2

Patches

2
b11eae9d82cb

XCOMMONS-1680: Filter Html attributes in restricted mode based on a whitelist

https://github.com/xwiki/xwiki-commonsMichael HamannJun 27, 2022via ghsa
6 files changed · +343 13
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLCleaner.java+6 1 modified
    @@ -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;
         }
     
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/filter/SanitizerFilter.java+246 0 added
    @@ -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;
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt+1 0 modified
    @@ -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
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/filter/AbstractHTMLFilterTest.java+16 0 modified
    @@ -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
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/html/HTMLUtilsTest.java+16 0 modified
    @@ -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
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLCleanerTest.java+58 12 modified
    @@ -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()}.
          *
    
4a185e0594d9

XCOMMONS-2426: Provide a component for filtering safe HTML elements and attributes

https://github.com/xwiki/xwiki-commonsMichael HamannJun 27, 2022via ghsa
14 files changed · +1484 1
  • xwiki-commons-core/xwiki-commons-xml/pom.xml+6 1 modified
    @@ -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>
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/checkstyle/checkstyle-suppressions.xml+5 0 modified
    @@ -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>
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/html/HTMLElementSanitizer.java+55 0 added
    @@ -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);
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizer.java+135 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLDefinitions.java+110 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/HTMLElementSanitizerConfiguration.java+144 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/InsecureHTMLElementSanitizer.java+52 0 added
    @@ -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;
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/MathMLDefinitions.java+119 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizer.java+233 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/java/org/xwiki/xml/internal/html/SVGDefinitions.java+171 0 added
    @@ -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);
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/main/resources/META-INF/components.txt+7 0 modified
    @@ -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
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/DefaultHTMLElementSanitizerTest.java+183 0 added
    @@ -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));
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/HTMLElementSanitizerTest.java+128 0 added
    @@ -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());
    +    }
    +}
    
  • xwiki-commons-core/xwiki-commons-xml/src/test/java/org/xwiki/xml/internal/html/SecureHTMLElementSanitizerTest.java+136 0 added
    @@ -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"));
    +    }
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.