You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2022/09/21 12:31:20 UTC
[sling-org-apache-sling-xss] branch master updated: SLING-7231 Move to owasp sanitizer library (#28)
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git
The following commit(s) were added to refs/heads/master by this push:
new 7a9ed4c SLING-7231 Move to owasp sanitizer library (#28)
7a9ed4c is described below
commit 7a9ed4c18cfacaa3a270ba0bc286b7d5e67cb00e
Author: Tatyana <36...@users.noreply.github.com>
AuthorDate: Wed Sep 21 14:31:16 2022 +0200
SLING-7231 Move to owasp sanitizer library (#28)
Co-authored-by: Julian Sedding <js...@apache.org>
Co-authored-by: Robert Munteanu <ro...@apache.org>
---
bnd.bnd | 28 +-
pom.xml | 64 ++--
src/main/java/javax/xml/parsers/FactoryFinder.java | 178 -----------
.../java/javax/xml/transform/FactoryFinder.java | 232 --------------
.../org/apache/sling/xss/impl/AntiSamyActions.java | 30 ++
.../sling/xss/impl/AntiSamyPolicyAdapter.java | 287 +++++++++++++++++
...AttributeTranslatingTransformerFactoryImpl.java | 55 ----
.../org/apache/sling/xss/impl/FallbackATag.java | 35 +--
.../apache/sling/xss/impl/FallbackSlingPolicy.java | 36 +--
.../org/apache/sling/xss/impl/HtmlSanitizer.java | 85 +++++
.../sling/xss/impl/HtmlToHtmlContentContext.java | 39 +--
.../sling/xss/impl/InvalidConfigException.java | 31 ++
.../org/apache/sling/xss/impl/PolicyHandler.java | 45 +--
.../org/apache/sling/xss/impl/SanitizedResult.java | 35 +++
.../java/org/apache/sling/xss/impl/XSSAPIImpl.java | 18 +-
.../org/apache/sling/xss/impl/XSSFilterImpl.java | 84 ++---
.../sling/xss/impl/status/XSSStatusService.java | 3 +-
.../sling/xss/impl/style/BatikCssCleaner.java | 82 +++++
.../apache/sling/xss/impl/style/CssValidator.java | 52 ++++
.../sling/xss/impl/style/StyleTagProcessor.java | 79 +++++
.../xss/impl/style/ValidatingDocumentHandler.java | 342 +++++++++++++++++++++
.../sling/xss/impl/xml/AllowedEmptyTags.java | 44 +++
.../apache/sling/xss/impl/xml/AntiSamyPolicy.java | 141 +++++++++
.../apache/sling/xss/impl/xml/AntiSamyRules.java | 127 ++++++++
.../sling/xss/impl/xml/AntiSamyXmlParser.java | 56 ++++
.../org/apache/sling/xss/impl/xml/Attribute.java | 139 +++++++++
.../org/apache/sling/xss/impl/xml/Category.java | 30 ++
.../org/apache/sling/xss/impl/xml/Directive.java | 37 +++
.../sling/xss/impl/xml/DynamicTagAttributes.java | 36 +++
.../sling/xss/impl/xml/GlobalTagAttributes.java | 36 +++
.../sling/xss/impl/xml/IncludeExcludeMatcher.java | 41 +++
.../org/apache/sling/xss/impl/xml/Literal.java | 43 +++
.../org/apache/sling/xss/impl/xml/MapBuilder.java | 246 +++++++++++++++
.../org/apache/sling/xss/impl/xml/Property.java | 125 ++++++++
.../java/org/apache/sling/xss/impl/xml/Regexp.java | 64 ++++
.../org/apache/sling/xss/impl/xml/Shorthand.java | 30 ++
.../java/org/apache/sling/xss/impl/xml/Tag.java | 85 +++++
.../org/apache/sling/xss/impl/xml/TagRules.java | 30 ++
.../apache/sling/xss/impl/xml/TagsToEncode.java | 36 +++
.../html/DynamicAttributesSanitizerPolicy.java | 151 +++++++++
.../javax.xml.transform.TransformerFactory | 18 --
.../apache/sling/xss/impl/AntiSamyPolicyTest.java | 144 ++++-----
.../org/apache/sling/xss/impl/XSSAPIImplTest.java | 11 +-
.../apache/sling/xss/impl/XSSFilterImplTest.java | 28 ++
.../org/apache/sling/xss/impl/xml/PolicyTest.java | 70 +++++
45 files changed, 2854 insertions(+), 754 deletions(-)
diff --git a/bnd.bnd b/bnd.bnd
index 45edbcb..e0e4581 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,5 +1,10 @@
Import-Package: !bsh, \
!nu.xom, \
+ !com.google.errorprone.annotations, \
+ !com.google.errorprone.annotations.concurrent, \
+ !javax.annotation, \
+ !org.checkerframework.checker.nullness.qual, \
+ !sun.misc, \
!org.apache.bcel.*, \
!org.apache.log4j.spi, \
!org.apache.log4j.xml, \
@@ -22,23 +27,20 @@ Import-Package: !bsh, \
!javax.mail.internet, \
!javax.servlet.jsp, \
!javax.servlet.jsp.tagext, \
- !javax.xml.*, \
!sun.io, \
*
Private-Package: org.apache.sling.xss.impl, \
org.apache.batik.*, \
+ com.google.common.base, \
+ com.google.common.collect, \
+ com.google.common.io, \
+ com.google.common.base.internal, \
+ com.google.common.graph, \
+ com.google.common.hash, \
+ com.google.common.math, \
+ com.google.common.primitives, \
org.w3c.css.sac, \
- org.apache.xalan.*, \
- org.apache.xerces.*, \
- org.apache.xml.*, \
- org.apache.xpath.*, \
- org.apache.xml.serialize, \
- org.apache.xml.serializer.*, \
- org.apache.commons.beanutils.*;-split-package:=merge-first, \
+ org.apache.commons.beanutils.*, \
org.apache.commons.configuration.*, \
org.apache.commons.logging.impl, \
- org.cyberneko.html.*, \
- org.owasp.*, \
- java_cup.runtime, \
- javax.xml.parsers;-split-package:=merge-first, \
- javax.xml.transform;-split-package:=merge-first
+ org.owasp.*;-split-package:=merge-first
diff --git a/pom.xml b/pom.xml
index f3e966e..cd8ad72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
<parent>
<groupId>org.apache.sling</groupId>
<artifactId>sling-bundle-parent</artifactId>
- <version>48</version>
+ <version>49</version>
<relativePath />
</parent>
@@ -44,7 +44,7 @@
<url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-xss.git</url>
<tag>HEAD</tag>
</scm>
-
+
<properties>
<project.build.outputTimestamp>1653046679</project.build.outputTimestamp>
</properties>
@@ -78,6 +78,8 @@
<ignore>javax.servlet.jsp.*</ignore>
<ignore>org.apache.avalon.*</ignore>
<ignore>org.apache.log.*</ignore>
+ <ignore>org.owasp.validator.html.*</ignore>
+ <ignore>org.w3c.dom.svg.*</ignore>
</ignores>
</configuration>
</plugin>
@@ -102,10 +104,6 @@
<configuration>
<outputDirectory>${project.build.directory}/dependencies-classes</outputDirectory>
<artifactItems>
- <artifactItem>
- <groupId>org.owasp.antisamy</groupId>
- <artifactId>antisamy</artifactId>
- </artifactItem>
<artifactItem>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
@@ -114,14 +112,6 @@
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder</artifactId>
</artifactItem>
- <artifactItem>
- <groupId>xalan</groupId>
- <artifactId>xalan</artifactId>
- </artifactItem>
- <artifactItem>
- <groupId>xml-apis</groupId>
- <artifactId>xml-apis</artifactId>
- </artifactItem>
</artifactItems>
</configuration>
</execution>
@@ -188,21 +178,21 @@
<!-- ======================================================================= -->
<dependencies>
<dependency>
- <groupId>org.owasp.antisamy</groupId>
- <artifactId>antisamy</artifactId>
- <version>1.6.4</version>
+ <groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
+ <artifactId>owasp-java-html-sanitizer</artifactId>
+ <version>20220608.1</version>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>xml-apis</groupId>
- <artifactId>xml-apis</artifactId>
- <version>1.4.01</version>
+ <groupId>com.fasterxml.jackson.dataformat</groupId>
+ <artifactId>jackson-dataformat-xml</artifactId>
+ <version>2.13.0</version>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>xalan</groupId>
- <artifactId>xalan</artifactId>
- <version>2.7.2</version>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <version>2.13.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -219,8 +209,30 @@
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
+ <exclusion>
+ <groupId>org.owasp.antisamy</groupId>
+ <artifactId>antisamy</artifactId>
+ </exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.xmlgraphics</groupId>
+ <artifactId>batik-css</artifactId>
+ <version>1.14</version>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>xml-apis</groupId>
+ <artifactId>xml-apis-ext</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.w3c.css</groupId>
+ <artifactId>sac</artifactId>
+ <version>1.3</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.owasp.encoder</groupId>
@@ -358,6 +370,12 @@
<version>1.0.1</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.7.25</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/javax/xml/parsers/FactoryFinder.java b/src/main/java/javax/xml/parsers/FactoryFinder.java
deleted file mode 100644
index 1ff4f95..0000000
--- a/src/main/java/javax/xml/parsers/FactoryFinder.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// $Id: FactoryFinder.java 670431 2008-06-23 01:40:03Z mrglavas $
-
-package javax.xml.parsers;
-
-/**
- * <p>This class is duplicated for each JAXP subpackage so keep it in
- * sync. It is package private.
- *
- * <p>This code is designed to implement the JAXP 1.1 spec pluggability
- * feature and is designed to run on JDK version 1.1 and later including
- * JVMs that perform early linking like the Microsoft JVM in IE 5. Note
- * however that it must be compiled on a JDK version 1.2 or later system
- * since it calls Thread#getContextClassLoader(). The code also runs both
- * as part of an unbundled jar file and when bundled as part of the JDK.
- *
- * <p><strong>NOTE</strong>:
- * <p>This class is included in order to make sure that all {@code javax.xml.parsers} factories are the ones embedded by the bundle
- * and not the ones provided by the underlying JVM or platform.
- *
- * <p>For more details check the following issues:
- * <ol>
- * <li>https://issues.apache.org/jira/browse/SLING-8321</li>
- * <li>https://issues.apache.org/jira/browse/SLING-8328</li>
- * </ol>
- */
-final class FactoryFinder {
-
- /**
- * <p>Debug flag to trace loading process.</p>
- */
- private static boolean debug = false;
-
- // Define system property "jaxp.debug" to get output
- static {
- // Use try/catch block to support applets, which throws
- // SecurityException out of this code.
- try {
- String val = System.getProperty("jaxp.debug");
- // Allow simply setting the prop to turn on debug
- debug = val != null && (! "false".equals(val));
- } catch (SecurityException se) {
- debug = false;
- }
- }
-
- private FactoryFinder() {}
-
- private static void dPrint(String msg) {
- if (debug) {
- System.err.println("JAXP: " + msg);
- }
- }
-
- /**
- * Create an instance of a class using the specified ClassLoader and
- * optionally fall back to the current ClassLoader if not found.
- *
- * @param className Name of the concrete class corresponding to the
- * service provider
- *
- * @param cl ClassLoader to use to load the class, null means to use
- * the bootstrap ClassLoader
- *
- * @param doFallback true if the current ClassLoader should be tried as
- * a fallback if the class is not found using cl
- */
- static Object newInstance(String className, ClassLoader cl,
- boolean doFallback)
- throws ConfigurationError
- {
- // assert(className != null);
-
- try {
- Class providerClass;
- if (cl == null) {
- // If classloader is null Use the bootstrap ClassLoader.
- // Thus Class.forName(String) will use the current
- // ClassLoader which will be the bootstrap ClassLoader.
- providerClass = Class.forName(className);
- } else {
- try {
- providerClass = cl.loadClass(className);
- } catch (ClassNotFoundException x) {
- if (doFallback) {
- // Fall back to current classloader
- cl = FactoryFinder.class.getClassLoader();
- if (cl != null) {
- providerClass = cl.loadClass(className);
- }
- else {
- providerClass = Class.forName(className);
- }
- } else {
- throw x;
- }
- }
- }
-
- Object instance = providerClass.newInstance();
- if (debug) dPrint("created new instance of " + providerClass +
- " using ClassLoader: " + cl);
- return instance;
- } catch (ClassNotFoundException x) {
- throw new ConfigurationError(
- "Provider " + className + " not found", x);
- } catch (Exception x) {
- throw new ConfigurationError(
- "Provider " + className + " could not be instantiated: " + x,
- x);
- }
- }
-
- /**
- * Finds the implementation Class object in the specified order. Main
- * entry point.
- * @return Class object of factory, never null
- *
- * @param factoryId Name of the factory to find, same as
- * a property name
- * @param fallbackClassName Implementation class name, if nothing else
- * is found. Use null to mean no fallback.
- *
- * Package private so this code can be shared.
- */
- static Object find(String factoryId, String fallbackClassName)
- throws ConfigurationError
- {
-
- // Figure out which ClassLoader to use for loading the provider
- // class. If there is a Context ClassLoader then use it.
-
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
- if (classLoader == null) {
- // if we have no Context ClassLoader
- // so use the current ClassLoader
- classLoader = FactoryFinder.class.getClassLoader();
- }
-
- if (debug) dPrint("loaded from fallback value: " + fallbackClassName);
- return newInstance(fallbackClassName, classLoader, true);
- }
-
- static class ConfigurationError extends Error {
- private Exception exception;
-
- /**
- * Construct a new instance with the specified detail string and
- * exception.
- */
- ConfigurationError(String msg, Exception x) {
- super(msg);
- this.exception = x;
- }
-
- Exception getException() {
- return exception;
- }
- }
-
-}
diff --git a/src/main/java/javax/xml/transform/FactoryFinder.java b/src/main/java/javax/xml/transform/FactoryFinder.java
deleted file mode 100644
index 7fee7be..0000000
--- a/src/main/java/javax/xml/transform/FactoryFinder.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// $Id: FactoryFinder.java 670431 2008-06-23 01:40:03Z mrglavas $
-
-package javax.xml.transform;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.commons.io.IOUtils;
-
-/**
- * <p>This class is duplicated for each JAXP subpackage so keep it in
- * sync. It is package private.
- *
- * <p>This code is designed to implement the JAXP 1.1 spec pluggability
- * feature and is designed to run on JDK version 1.1 and later including
- * JVMs that perform early linking like the Microsoft JVM in IE 5. Note
- * however that it must be compiled on a JDK version 1.2 or later system
- * since it calls Thread#getContextClassLoader(). The code also runs both
- * as part of an unbundled jar file and when bundled as part of the JDK.
- *
- * <p>This class does not support the full transformer factory lookup mechanism.
- * It only supports lookups via the <tt>ServiceLoader</tt> mechanism. By virtue of the
- * libraries embedded in the Sling XSS bundle, the fallback <tt>TransformerFactory</tt>
- * implementation is the one provided by Xalan.</p>
- *
- * <p><strong>NOTE</strong>:
- * <p>This class is included in order to make sure that all {@code javax.xml.transform} factories are the ones embedded by the bundle
- * and not the ones provided by the underlying JVM or platform.
- *
- * <p>For more details check the following issues:
- * <ol>
- * <li>https://issues.apache.org/jira/browse/SLING-8321</li>
- * <li>https://issues.apache.org/jira/browse/SLING-8328</li>
- * <li>https://issues.apache.org/jira/browse/SLING-10953</li>
- * </ol>
- */
-final class FactoryFinder {
-
- /**
- * <p>Debug flag to trace loading process.</p>
- */
- private static boolean debug = false;
-
- // Define system property "jaxp.debug" to get output
- static {
- // Use try/catch block to support applets, which throws
- // SecurityException out of this code.
- try {
- String val = System.getProperty("jaxp.debug");
- // Allow simply setting the prop to turn on debug
- debug = val != null && (! "false".equals(val));
- } catch (SecurityException se) {
- debug = false;
- }
- }
-
- private FactoryFinder() {}
-
- private static void dPrint(String msg) {
- if (debug) {
- System.err.println("JAXP: " + msg);
- }
- }
-
- /**
- * Create an instance of a class using the specified ClassLoader and
- * optionally fall back to the current ClassLoader if not found.
- *
- * @param className Name of the concrete class corresponding to the
- * service provider
- *
- * @param cl ClassLoader to use to load the class, null means to use
- * the bootstrap ClassLoader
- *
- * @param doFallback true if the current ClassLoader should be tried as
- * a fallback if the class is not found using cl
- */
- static Object newInstance(String className, ClassLoader cl,
- boolean doFallback)
- throws ConfigurationError
- {
- // assert(className != null);
-
- try {
- Class providerClass;
- if (cl == null) {
- // If classloader is null Use the bootstrap ClassLoader.
- // Thus Class.forName(String) will use the current
- // ClassLoader which will be the bootstrap ClassLoader.
- providerClass = Class.forName(className);
- } else {
- try {
- providerClass = cl.loadClass(className);
- } catch (ClassNotFoundException x) {
- if (doFallback) {
- // Fall back to current classloader
- cl = FactoryFinder.class.getClassLoader();
- if (cl != null) {
- providerClass = cl.loadClass(className);
- }
- else {
- providerClass = Class.forName(className);
- }
- } else {
- throw x;
- }
- }
- }
-
- Object instance = providerClass.newInstance();
- if (debug) dPrint("created new instance of " + providerClass +
- " using ClassLoader: " + cl);
- return instance;
- } catch (ClassNotFoundException x) {
- throw new ConfigurationError(
- "Provider " + className + " not found", x);
- } catch (Exception x) {
- throw new ConfigurationError(
- "Provider " + className + " could not be instantiated: " + x,
- x);
- }
- }
-
- /**
- * Finds the implementation Class object in the specified order. Main
- * entry point.
- * @return Class object of factory, never null
- *
- * @param factoryId Name of the factory to find, same as
- * a property name
- * @param fallbackClassName Implementation class name, if nothing else
- * is found. Use null to mean no fallback.
- *
- * Package private so this code can be shared.
- */
- static Object find(String factoryId, String fallbackClassName)
- throws ConfigurationError
- {
-
- // Figure out which ClassLoader to use for loading the provider
- // class. If there is a Context ClassLoader then use it.
-
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
- if (classLoader == null) {
- // if we have no Context ClassLoader
- // so use the current ClassLoader
- classLoader = FactoryFinder.class.getClassLoader();
- }
-
- Object instance = loadFactoryFromServiceFile(factoryId, classLoader);
- if ( instance != null )
- return instance;
-
- if (debug) dPrint("loaded from fallback value: " + fallbackClassName);
- return newInstance(fallbackClassName, classLoader, true);
- }
-
- private static Object loadFactoryFromServiceFile(String factoryId, ClassLoader classLoader) {
- try {
- Enumeration<URL> serviceFiles = classLoader.getResources("/META-INF/services/" + factoryId);
- while ( serviceFiles.hasMoreElements() ) {
- URL url = serviceFiles.nextElement();
- if ( debug ) {
- dPrint("Inspecting service file " + url );
- }
- try ( InputStream is = url.openStream() ) {
- List<String> lines = IOUtils.readLines(is, StandardCharsets.UTF_8);
- Optional<String> service = lines.stream()
- .filter( s -> ! s.trim().startsWith("#") )
- .filter( s -> ! s.trim().isEmpty())
- .findFirst();
-
- if ( service.isPresent() ) {
- try {
- return newInstance(service.get(), classLoader, true);
- } catch (ConfigurationError e) {
- // continue
- }
- }
- }
- }
- } catch (IOException e) {
- if (debug) {
- dPrint("Failed loading service files " + e.getMessage());
- e.printStackTrace(System.err);
- }
- }
-
- return null;
- }
-
- static class ConfigurationError extends Error {
- private Exception exception;
-
- /**
- * Construct a new instance with the specified detail string and
- * exception.
- */
- ConfigurationError(String msg, Exception x) {
- super(msg);
- this.exception = x;
- }
-
- Exception getException() {
- return exception;
- }
- }
-
-}
diff --git a/src/main/java/org/apache/sling/xss/impl/AntiSamyActions.java b/src/main/java/org/apache/sling/xss/impl/AntiSamyActions.java
new file mode 100644
index 0000000..68839c8
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/AntiSamyActions.java
@@ -0,0 +1,30 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl;
+
+public class AntiSamyActions {
+ private AntiSamyActions() {
+ throw new IllegalStateException("Utility class");
+ }
+ public static final String REMOVE_ATTRIBUTE_ON_INVALID = "removeAttribute";
+ static final String TRUNCATE = "truncate";
+ static final String FILTER = "filter";
+ static final String REMOVE = "remove";
+ static final String VALIDATE = "validate";
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java b/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java
new file mode 100644
index 0000000..bacb4f9
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/AntiSamyPolicyAdapter.java
@@ -0,0 +1,287 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.sling.xss.impl.style.CssValidator;
+import org.apache.sling.xss.impl.xml.Attribute;
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
+import org.apache.sling.xss.impl.xml.Tag;
+import org.jetbrains.annotations.Nullable;
+import org.owasp.html.AttributePolicy;
+import org.owasp.html.HtmlPolicyBuilder;
+import org.owasp.html.PolicyFactory;
+
+import com.google.common.base.Predicate;
+
+public class AntiSamyPolicyAdapter {
+ private static final String ALLOW_DYNAMIC_ATTRIBUTES = "allowDynamicAttributes";
+ private static final String REMOVE_TAG_ON_INVALID_ACTION = "removeTag";
+
+ private final List<String> onInvalidRemoveTagList = new ArrayList<>();
+ private final Map<String, AttributePolicy> dynamicAttributesPolicyMap = new HashMap<>();
+
+ private PolicyFactory policyFactory;
+ private CssValidator cssValidator;
+
+ public AntiSamyPolicyAdapter(AntiSamyPolicy policy) {
+ removeAttributeGuards();
+ HtmlPolicyBuilder policyBuilder = new HtmlPolicyBuilder();
+
+ cssValidator = new CssValidator(policy.getCssPolicy());
+
+ // ------------ this is for the global attributes -------------
+ Map<String, Attribute> globalAttributes = policy.getGlobalAttributes();
+
+ for (Attribute attribute : globalAttributes.values()) {
+ if (attribute.getOnInvalid().equals(REMOVE_TAG_ON_INVALID_ACTION)) {
+ onInvalidRemoveTagList.add(attribute.getName());
+ }
+
+ if (CssValidator.STYLE_ATTRIBUTE_NAME.equals(attribute.getName())) {
+ // we match style tags separately
+ policyBuilder.allowAttributes(attribute.getName()).matching(cssValidator.newCssAttributePolicy())
+ .globally();
+ } else {
+ List<String> allowedValuesFromAttribute = attribute.getLiterals();
+ for (String allowedValue : allowedValuesFromAttribute) {
+ policyBuilder.allowAttributes(attribute.getName()).matching(true, allowedValue).globally();
+ }
+
+ List<Pattern> regexsFromAttribute = attribute.getPatternList();
+ if (!regexsFromAttribute.isEmpty()) {
+ policyBuilder.allowAttributes(attribute.getName()).matching(matchesToPatterns(regexsFromAttribute))
+ .globally();
+ } else {
+ policyBuilder.allowAttributes(attribute.getName()).globally();
+ }
+ }
+ }
+
+ // ------------ this is for the allowed empty tags -------------
+ List<String> allowedEmptyTags = policy.getAllowedEmptyTags();
+ for (String allowedEmptyTag : allowedEmptyTags) {
+ policyBuilder.allowWithoutAttributes(allowedEmptyTag);
+ }
+
+ // ------------ this is for the tag rules -------------
+ Map<String, Tag> tagMap = policy.getTagRules();
+ for (Map.Entry<String, Tag> tag : tagMap.entrySet()) {
+
+ String tagAction = tag.getValue().getAction();
+ switch (tagAction) {
+ // Tag.action
+ case AntiSamyActions.TRUNCATE:
+ policyBuilder.allowElements(tag.getValue().getName());
+ break;
+
+ // filter: remove tags, but keep content,
+ case AntiSamyActions.FILTER:
+ break;
+
+ // remove: remove tag and contents
+ case AntiSamyActions.REMOVE:
+ policyBuilder.disallowElements(tag.getValue().getName());
+ break;
+
+ case AntiSamyActions.VALIDATE:
+ case "":
+ policyBuilder.allowElements(tag.getValue().getName());
+ boolean styleSeen = false;
+ // get the allowed Attributes for the tag
+ Map<String, Attribute> allowedAttributes = tag.getValue().getAttributeMap();
+ // if there are allowed Attributes, map over them
+ for (Attribute attribute : allowedAttributes.values()) {
+
+ if (attribute.getOnInvalid().equals(REMOVE_TAG_ON_INVALID_ACTION)) {
+ onInvalidRemoveTagList.add(attribute.getName());
+ }
+
+ styleSeen = CssValidator.STYLE_ATTRIBUTE_NAME.equals(attribute.getName());
+
+ List<String> literalList = attribute.getLiterals();
+ List<Pattern> patternList = attribute.getPatternList();
+
+ if (!literalList.isEmpty() && !patternList.isEmpty()) {
+ // if both, the patterns and the literals are not empty, the value should be checked with them with an OR and not with an AND.
+ policyBuilder.allowAttributes(attribute.getName())
+ .matching(matchesPatternsOrLiterals(patternList, true, literalList))
+ .onElements(tag.getValue().getName());
+ }
+ else if (!literalList.isEmpty()) {
+ policyBuilder.allowAttributes(attribute.getName())
+ .matching(true, literalList.toArray(new String[0]))
+ .onElements(tag.getValue().getName());
+ policyBuilder.allowAttributes(attribute.getName()).onElements(tag.getValue().getName());
+ }
+ else if (!patternList.isEmpty()) {
+ policyBuilder.allowAttributes(attribute.getName())
+ .matching(matchesToPatterns(patternList))
+ .onElements(tag.getValue().getName());
+ }
+ }
+
+ if (!styleSeen) {
+ policyBuilder.allowAttributes(CssValidator.STYLE_ATTRIBUTE_NAME)
+ .matching(cssValidator.newCssAttributePolicy()).onElements(tag.getValue().getName());
+ }
+ break;
+
+ default:
+ throw new IllegalArgumentException("No tag action found.");
+ }
+ }
+
+ // disallow style tag on specific elements
+ policyBuilder.disallowAttributes(CssValidator.STYLE_ATTRIBUTE_NAME)
+ .onElements(cssValidator.getDisallowedTagNames().toArray(new String[0]));
+
+ // ---------- dynamic attributes ------------
+ Map<String, Attribute> dynamicAttributes = new HashMap<>();
+
+ // checks if the dynamic attributes are allowed
+ if (policy.getDirectives().get(ALLOW_DYNAMIC_ATTRIBUTES).equals("true")) {
+ dynamicAttributes.putAll(policy.getDynamicAttributes());
+ for (Attribute attribute : dynamicAttributes.values()) {
+ if (attribute.getOnInvalid().equals(REMOVE_TAG_ON_INVALID_ACTION)) {
+ onInvalidRemoveTagList.add(attribute.getName());
+ }
+
+ List<Pattern> regexsFromAttribute = attribute.getPatternList();
+ for (Pattern regex : regexsFromAttribute) {
+ dynamicAttributesPolicyMap.put(attribute.getName(), newDynamicAttributePolicy(regex));
+ }
+
+ List<String> allowedValuesFromAttribute = attribute.getLiterals();
+ if (!allowedValuesFromAttribute.isEmpty()) {
+ dynamicAttributesPolicyMap.put(attribute.getName(),
+ newDynamicAttributePolicy(true, allowedValuesFromAttribute.toArray(new String[0])));
+ }
+
+ }
+ }
+
+ policyFactory = policyBuilder.allowTextIn(CssValidator.STYLE_TAG_NAME).toFactory();
+
+ }
+
+ public PolicyFactory getHtmlCleanerPolicyFactory() {
+ return policyFactory;
+ }
+
+ public Map<String, AttributePolicy> getDynamicAttributesPolicyMap() {
+ return dynamicAttributesPolicyMap;
+ }
+
+ public List<String> getOnInvalidRemoveTagList() {
+ return onInvalidRemoveTagList;
+ }
+
+ public CssValidator getCssValidator() {
+ return cssValidator;
+ }
+
+ private static Predicate<String> matchesToPatterns(List<Pattern> patternList) {
+ return new Predicate<String>() {
+ @Override
+ public boolean apply(String s) {
+ for (Pattern pattern : patternList) {
+ if (pattern.matcher(s).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ private static Predicate<String> matchesPatternsOrLiterals(List<Pattern> patternList, boolean ignoreCase, List<String> literalList) {
+ return new Predicate<String>() {
+ public boolean apply(String s) {
+ // check if the string matches to the pattern
+ for (Pattern pattern : patternList) {
+ if (pattern.matcher(s).matches()) {
+ return true;
+ }
+ }
+ // if the pattern does not match it goes through the literals
+ for (String string : literalList) {
+ s = ignoreCase
+ ? s.toLowerCase()
+ : s;
+ if (string.equals(s)) {
+ return true;
+ }
+ }
+ // if it neither matches the patterns nor the literals it returns false
+ return false;
+ }
+ };
+ }
+
+ public AttributePolicy newDynamicAttributePolicy(final Pattern pattern) {
+ return new AttributePolicy() {
+ @Override
+ public @Nullable String apply(String elementName, String attributeName, String value) {
+ return pattern.matcher(value).matches() ? value : null;
+ }
+ };
+ }
+
+ public AttributePolicy newDynamicAttributePolicy(boolean ignoreCase, String... allowedValues) {
+ final List<String> allowed = Arrays.asList(allowedValues);
+ return new AttributePolicy() {
+ @Override
+ public @Nullable String apply(String elementName, String attributeName, String uncanonValue) {
+ String value = ignoreCase ? uncanonValue.toLowerCase() : uncanonValue;
+ return allowed.contains(value) ? value : null;
+ }
+ };
+ }
+
+ // java html sanitizer has some default Attribute Guards, which we don't want.
+ // So we are removing them here
+ private void removeAttributeGuards() {
+ try {
+ Field guards = HtmlPolicyBuilder.class.getDeclaredField("ATTRIBUTE_GUARDS");
+ letMeIn(guards);
+ guards.set(null, new HashMap<>());
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private void letMeIn(Field field) throws ReflectiveOperationException {
+ if (!field.isAccessible())
+ field.setAccessible(true);
+ if ((field.getModifiers() & Modifier.FINAL) != 0) {
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/AttributeTranslatingTransformerFactoryImpl.java b/src/main/java/org/apache/sling/xss/impl/AttributeTranslatingTransformerFactoryImpl.java
deleted file mode 100644
index d8c04aa..0000000
--- a/src/main/java/org/apache/sling/xss/impl/AttributeTranslatingTransformerFactoryImpl.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ~ Licensed to the Apache Software Foundation (ASF) under one
- ~ or more contributor license agreements. See the NOTICE file
- ~ distributed with this work for additional information
- ~ regarding copyright ownership. The ASF licenses this file
- ~ to you under the Apache License, Version 2.0 (the
- ~ "License"); you may not use this file except in compliance
- ~ with the License. You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing,
- ~ software distributed under the License is distributed on an
- ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- ~ KIND, either express or implied. See the License for the
- ~ specific language governing permissions and limitations
- ~ under the License.
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
-package org.apache.sling.xss.impl;
-
-import static javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD;
-import static javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET;
-import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
-
-import javax.xml.XMLConstants;
-import javax.xml.transform.TransformerConfigurationException;
-
-import org.apache.xalan.processor.TransformerFactoryImpl;
-
-/**
- * Translates configuration calls to specific unsupported attributes to the {@link XMLConstants#FEATURE_SECURE_PROCESSING} feature
- *
- * <p>This is done in order to support AntiSamy 1.6.4 which requires the usage of a JAXP 1.5 compliant
- * transformer factory, which is Xalan is not. This implementation is minimal and not expected to be used
- * outside of this bundle.</p>
- *
- * @see <a href="https://github.com/nahsra/antisamy/issues/103">AntiSamy issue 103</a>
- */
-public class AttributeTranslatingTransformerFactoryImpl extends TransformerFactoryImpl {
-
- @Override
- public void setAttribute(String name, Object value) throws IllegalArgumentException {
- if ( "".equals(value) && (
- ACCESS_EXTERNAL_DTD.equals(name) ||
- ACCESS_EXTERNAL_STYLESHEET.equals(name) ) ) {
- try {
- setFeature(FEATURE_SECURE_PROCESSING, true);
- return;
- } catch (TransformerConfigurationException e) {
- throw new IllegalArgumentException("Failed translating attribute " + name + " to feature " + FEATURE_SECURE_PROCESSING ,e);
- }
- }
- super.setAttribute(name, value);
- }
-}
diff --git a/src/main/java/org/apache/sling/xss/impl/FallbackATag.java b/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
index a94de65..c2e5266 100644
--- a/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
+++ b/src/main/java/org/apache/sling/xss/impl/FallbackATag.java
@@ -18,29 +18,19 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.xss.impl;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
-import org.owasp.validator.html.model.Attribute;
-import org.owasp.validator.html.model.Tag;
+import org.apache.sling.xss.impl.xml.Attribute;
+import org.apache.sling.xss.impl.xml.Tag;
public class FallbackATag extends Tag {
- static final Attribute FALLBACK_HREF_ATTRIBUTE = new Attribute(
- "href",
- Arrays.asList(
- XSSFilterImpl.ON_SITE_SIMPLIFIED,
- XSSFilterImpl.OFF_SITE_SIMPLIFIED
- ),
- Collections.emptyList(),
- "removeAttribute", ""
- );
-
private final Tag wrapped;
public FallbackATag(Tag wrapped) {
- super("a", new HashMap<>(), "validate");
+ super("a", AntiSamyActions.VALIDATE, new ArrayList<>());
this.wrapped = wrapped;
}
@@ -60,8 +50,8 @@ public class FallbackATag extends Tag {
}
@Override
- public String getRegularExpression() {
- return wrapped.getRegularExpression();
+ public List<Attribute> getAttributeList() {
+ return wrapped.getAttributeList();
}
@Override
@@ -69,10 +59,17 @@ public class FallbackATag extends Tag {
return wrapped.getName();
}
+ @Override
+ public Map<String, Attribute> getAttributeMap() {
+ Map<String, Attribute> map = wrapped.getAttributeMap();
+ map.put("href", XSSFilterImpl.FALLBACK_HREF_ATTRIBUTE);
+ return map;
+ }
+
@Override
public Attribute getAttributeByName(String name) {
if ("href".equalsIgnoreCase(name)) {
- return FALLBACK_HREF_ATTRIBUTE;
+ return XSSFilterImpl.FALLBACK_HREF_ATTRIBUTE;
}
return wrapped.getAttributeByName(name);
}
diff --git a/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java b/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
index 5e8145e..16ac7f4 100644
--- a/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
+++ b/src/main/java/org/apache/sling/xss/impl/FallbackSlingPolicy.java
@@ -18,38 +18,24 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.xss.impl;
+import java.io.IOException;
import java.io.InputStream;
-import org.owasp.validator.html.InternalPolicy;
-import org.owasp.validator.html.PolicyException;
-import org.owasp.validator.html.model.Tag;
-import org.xml.sax.InputSource;
+import javax.xml.stream.XMLStreamException;
-public class FallbackSlingPolicy extends InternalPolicy {
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
+import org.apache.sling.xss.impl.xml.Tag;
- private FallbackATag fallbackATag;
- private final Object aTagLock = new Object();
+public class FallbackSlingPolicy extends AntiSamyPolicy {
- public FallbackSlingPolicy(InputStream inputStream) throws PolicyException {
- super(getSimpleParseContext(getTopLevelElement(new InputSource(inputStream), () -> new InputSource(inputStream))));
+ public FallbackSlingPolicy(InputStream inputStream) throws InvalidConfigException, XMLStreamException, IOException {
- }
+ super(inputStream);
- @Override
- public Tag getTagByLowercaseName(String tagName) {
- if ("a".equalsIgnoreCase(tagName)) {
- synchronized (aTagLock) {
- if (fallbackATag == null) {
- Tag wrapped = super.getTagByLowercaseName(tagName);
- if (wrapped != null) {
- fallbackATag = new FallbackATag(wrapped);
- }
- }
- }
- if (fallbackATag != null) {
- return fallbackATag;
- }
+ Tag original = tagRules.get("a");
+ if (original != null) {
+ Tag wrapped = new FallbackATag(original);
+ tagRules.put("a", wrapped);
}
- return super.getTagByLowercaseName(tagName);
}
}
diff --git a/src/main/java/org/apache/sling/xss/impl/HtmlSanitizer.java b/src/main/java/org/apache/sling/xss/impl/HtmlSanitizer.java
new file mode 100644
index 0000000..777068a
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/HtmlSanitizer.java
@@ -0,0 +1,85 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl;
+
+import java.lang.reflect.Field;
+import java.util.Objects;
+
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
+import org.owasp.html.DynamicAttributesSanitizerPolicy;
+import org.owasp.html.Handler;
+import org.owasp.html.HtmlStreamEventReceiver;
+import org.owasp.html.HtmlStreamRenderer;
+import org.owasp.html.PolicyFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class HtmlSanitizer {
+
+ private AntiSamyPolicyAdapter customPolicy;
+ private ImmutableMap policies;
+ private ImmutableSet<String> textContainers;
+
+ public HtmlSanitizer(AntiSamyPolicy policy) {
+ this.customPolicy = new AntiSamyPolicyAdapter(policy);
+ policies = reflectionGetPolicies(customPolicy.getHtmlCleanerPolicyFactory());
+ textContainers = reflectionGetTextContainers(customPolicy.getHtmlCleanerPolicyFactory());
+ }
+
+ public SanitizedResult scan(String taintedHTML) {
+ StringBuilder sb = new StringBuilder(taintedHTML.length());
+ HtmlStreamEventReceiver out = HtmlStreamRenderer.create(sb, Handler.DO_NOTHING);
+ DynamicAttributesSanitizerPolicy dynamicPolicy = new DynamicAttributesSanitizerPolicy(out, policies,
+ textContainers, customPolicy.getDynamicAttributesPolicyMap(), customPolicy.getOnInvalidRemoveTagList());
+
+ org.owasp.html.HtmlSanitizer.sanitize(taintedHTML, dynamicPolicy,
+ customPolicy.getCssValidator().newStyleTagProcessor());
+ return new SanitizedResult(sb.toString(), dynamicPolicy.getNumberOfErrors());
+ }
+
+ private ImmutableSet<String> reflectionGetTextContainers(PolicyFactory policyFactory) {
+ Class<?> c = policyFactory.getClass();
+ try {
+ Field field = c.getDeclaredField("textContainers");
+ field.setAccessible(true);
+ return (ImmutableSet<String>) field.get(policyFactory);
+ } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private ImmutableMap reflectionGetPolicies(PolicyFactory policyFactory) {
+ Class<?> c = policyFactory.getClass();
+ try {
+ Field field = c.getDeclaredField("policies");
+ field.setAccessible(true);
+ return (ImmutableMap) field.get(policyFactory);
+ } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public SanitizedResult scan(String taintedHTML, AntiSamyPolicy policy) {
+ Objects.requireNonNull(taintedHTML, "Null html input");
+ Objects.requireNonNull(policy, "Null policy loaded");
+
+ return new HtmlSanitizer(policy).scan(taintedHTML);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/HtmlToHtmlContentContext.java b/src/main/java/org/apache/sling/xss/impl/HtmlToHtmlContentContext.java
index d387033..8229f1d 100644
--- a/src/main/java/org/apache/sling/xss/impl/HtmlToHtmlContentContext.java
+++ b/src/main/java/org/apache/sling/xss/impl/HtmlToHtmlContentContext.java
@@ -16,12 +16,8 @@
******************************************************************************/
package org.apache.sling.xss.impl;
-import java.util.List;
import org.apache.commons.lang3.StringUtils;
-import org.owasp.validator.html.CleanResults;
-import org.owasp.validator.html.PolicyException;
-import org.owasp.validator.html.ScanException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,14 +41,10 @@ public class HtmlToHtmlContentContext implements XSSFilterRule {
@Override
public boolean check(final PolicyHandler policyHandler, final String str) {
if (StringUtils.isNotEmpty(str)) {
- ClassLoader tccl = Thread.currentThread().getContextClassLoader();
try {
- Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
- return policyHandler.getAntiSamy().scan(str).getNumberOfErrors() == 0;
+ return getCleanResults(policyHandler, str).getNumberOfErrors() == 0;
} catch (final Exception se) {
logError(se, str);
- } finally {
- Thread.currentThread().setContextClassLoader(tccl);
}
}
return false;
@@ -62,21 +54,16 @@ public class HtmlToHtmlContentContext implements XSSFilterRule {
* @see XSSFilterRule#filter(PolicyHandler, java.lang.String)
*/
@Override
- public String filter(final PolicyHandler policyHandler, final String str) {
- if (StringUtils.isNotEmpty(str)) {
+ public String filter(final PolicyHandler policyHandler, final String unsafeString) {
+ if (StringUtils.isNotEmpty(unsafeString)) {
try {
- final CleanResults results = getCleanResults(policyHandler, str);
+ final String results = getCleanResults(policyHandler, unsafeString).getSanitizedString();
if (results != null) {
- final String cleaned = results.getCleanHTML();
- final List<String> errors = results.getErrorMessages();
- for (final String error : errors) {
- log.info("AntiSamy warning: {}", error);
- }
- log.debug("Protected (HTML -> HTML):\n{}", cleaned);
- return cleaned;
+ log.debug("Protected (HTML -> HTML):\n{}", results);
+ return results;
}
} catch (Exception e) {
- logError(e, str);
+ logError(e, unsafeString);
}
}
return StringUtils.EMPTY;
@@ -90,18 +77,14 @@ public class HtmlToHtmlContentContext implements XSSFilterRule {
return true;
}
- private CleanResults getCleanResults(PolicyHandler handler, String input) throws ScanException, PolicyException {
- CleanResults results;
- ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ private SanitizedResult getCleanResults(PolicyHandler handler, String input) {
+ SanitizedResult results;
try {
- Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
- results = handler.getAntiSamy().scan(input);
+ results = handler.getHtmlSanitizer().scan(input);
} catch (StackOverflowError e) {
log.debug("Will perform a second attempt at filtering the following input due to a StackOverflowError:\n{}", input);
- results = handler.getFallbackAntiSamy().scan(input);
+ results = handler.getFallbackHtmlSanitizer().scan(input);
log.debug("Second attempt was successful.");
- } finally {
- Thread.currentThread().setContextClassLoader(tccl);
}
return results;
}
diff --git a/src/main/java/org/apache/sling/xss/impl/InvalidConfigException.java b/src/main/java/org/apache/sling/xss/impl/InvalidConfigException.java
new file mode 100644
index 0000000..0501001
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/InvalidConfigException.java
@@ -0,0 +1,31 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl;
+
+public class InvalidConfigException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidConfigException(Exception e) {
+ super(e);
+ }
+
+ public InvalidConfigException(String s) {
+ super(s);
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/PolicyHandler.java b/src/main/java/org/apache/sling/xss/impl/PolicyHandler.java
index 5568983..9b4c257 100644
--- a/src/main/java/org/apache/sling/xss/impl/PolicyHandler.java
+++ b/src/main/java/org/apache/sling/xss/impl/PolicyHandler.java
@@ -21,61 +21,44 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
-import org.owasp.validator.html.AntiSamy;
-import org.owasp.validator.html.Policy;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
/**
* Class that provides the capability of securing input provided as plain text for HTML output.
*/
public class PolicyHandler {
-
- private final Logger logger = LoggerFactory.getLogger(getClass());
-
- private final Policy policy;
- private Policy fallbackPolicy;
- private AntiSamy antiSamy;
- private AntiSamy fallbackAntiSamy;
+ private final AntiSamyPolicy policy;
+ private AntiSamyPolicy fallbackPolicy;
+ private HtmlSanitizer htmlSanitizer;
+ private HtmlSanitizer fallbackHtmlSanitizer;
/**
* Creates a {@code PolicyHandler} from an {@link InputStream}.
*
- * @param policyStream the InputStream from which to read this handler's {@link Policy}
+ * @param policyStream the InputStream from which to read this handler's {@link AntiSamyPolicy}
*/
public PolicyHandler(InputStream policyStream) throws Exception {
- // fix for classloader issue with IBM JVM: see bug #31946
- // (currently: http://bugs.day.com/bugzilla/show_bug.cgi?id=31946)
- Thread currentThread = Thread.currentThread();
- ClassLoader cl = currentThread.getContextClassLoader();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- currentThread.setContextClassLoader(this.getClass().getClassLoader());
IOUtils.copy(policyStream, baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- currentThread.setContextClassLoader(this.getClass().getClassLoader());
- this.policy = Policy.getInstance(bais);
- if ( "true".equals(this.policy.getDirective(Policy.EMBED_STYLESHEETS)) ) {
- logger.warn("The AntiSamy directive {} is set to true. This directive is deprecated and will not be supported in future Sling XSS bundle releases", Policy.EMBED_STYLESHEETS);
- }
+ this.policy = new AntiSamyPolicy(bais);
bais.reset();
+ this.htmlSanitizer = new HtmlSanitizer(this.policy);
this.fallbackPolicy = new FallbackSlingPolicy(bais);
- this.antiSamy = new AntiSamy(this.policy);
- this.fallbackAntiSamy = new AntiSamy(this.fallbackPolicy);
- } finally {
- currentThread.setContextClassLoader(cl);
+ this.fallbackHtmlSanitizer = new HtmlSanitizer(this.fallbackPolicy);
}
}
- public Policy getPolicy() {
+ public AntiSamyPolicy getPolicy() {
return this.policy;
}
- public AntiSamy getAntiSamy() {
- return this.antiSamy;
+ public HtmlSanitizer getHtmlSanitizer() {
+ return this.htmlSanitizer;
}
- public AntiSamy getFallbackAntiSamy() {
- return fallbackAntiSamy;
+ public HtmlSanitizer getFallbackHtmlSanitizer() {
+ return fallbackHtmlSanitizer;
}
}
diff --git a/src/main/java/org/apache/sling/xss/impl/SanitizedResult.java b/src/main/java/org/apache/sling/xss/impl/SanitizedResult.java
new file mode 100644
index 0000000..561fee4
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/SanitizedResult.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or
+ * more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to you under the
+ * Apache License, Version 2.0 (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ ******************************************************************************/
+package org.apache.sling.xss.impl;
+
+public class SanitizedResult {
+ private String sanitizedString;
+ private int numberOfErrors;
+
+ public SanitizedResult(String sanitizedString, int numberOfErrors) {
+ this.sanitizedString = sanitizedString;
+ this.numberOfErrors = numberOfErrors;
+ }
+
+ public String getSanitizedString() {
+ return sanitizedString;
+ }
+
+ public int getNumberOfErrors() {
+ return numberOfErrors;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
index 68ed5c9..f5d5e88 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
@@ -67,9 +67,6 @@ public class XSSAPIImpl implements XSSAPI {
@Activate
protected void activate() {
- ClassLoader tccl = Thread.currentThread().getContextClassLoader();
- try {
- Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
@@ -83,9 +80,6 @@ public class XSSAPIImpl implements XSSAPI {
Map<String, Object> config = new HashMap<>();
config.put("org.apache.johnzon.supports-comments", true);
jsonReaderFactory = Json.createReaderFactory(config);
- } finally {
- Thread.currentThread().setContextClassLoader(tccl);
- }
}
@Deactivate
@@ -186,12 +180,12 @@ public class XSSAPIImpl implements XSSAPI {
// Percent-encode characters that are not allowed in unquoted
// HTML attributes: ", ', >, <, ` and space. We don't encode =
// since this would break links with query parameters.
- String encodedUrl = url.replaceAll("\"", "%22")
- .replaceAll("'", "%27")
- .replaceAll(">", "%3E")
- .replaceAll("<", "%3C")
- .replaceAll("`", "%60")
- .replaceAll(" ", "%20");
+ String encodedUrl = url.replace("\"", "%22")
+ .replace("'", "%27")
+ .replace(">", "%3E")
+ .replace("<", "%3C")
+ .replace("`", "%60")
+ .replace(" ", "%20");
try {
if (xssFilter.isValidHref(encodedUrl)) {
return encodedUrl;
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
index cadcd75..ade0fe8 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
@@ -42,6 +42,9 @@ import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.apache.sling.xss.ProtectionContext;
import org.apache.sling.xss.XSSFilter;
import org.apache.sling.xss.impl.status.XSSStatusService;
+import org.apache.sling.xss.impl.xml.Attribute;
+import org.apache.sling.xss.impl.xml.Regexp;
+import org.apache.sling.xss.impl.xml.Tag;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
@@ -53,8 +56,6 @@ import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
-import org.owasp.validator.html.model.Attribute;
-import org.owasp.validator.html.model.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -136,7 +137,13 @@ public class XSSFilterImpl implements XSSFilter {
static final Pattern OFF_SITE_SIMPLIFIED = Pattern.compile("(\\s)*((ht|f)tp(s?)://|mailto:)" +
"[\\p{L}\\p{N}]+[\\p{L}\\p{N}\\p{Zs}\\.\\#@\\$%\\+&;:\\-_~,\\?=/!\\*\\(\\)]*(\\s)*");
- private static final Pattern[] BACKUP_PATTERNS = new Pattern[] {ON_SITE_SIMPLIFIED, OFF_SITE_SIMPLIFIED};
+ static final Attribute FALLBACK_HREF_ATTRIBUTE = new Attribute(
+ "href",
+ Arrays.asList(
+ new Regexp("on-site-simplified", ON_SITE_SIMPLIFIED.toString()),
+ new Regexp("off-site-simplified", OFF_SITE_SIMPLIFIED.toString())),
+ Collections.emptyList(),
+ AntiSamyActions.REMOVE_ATTRIBUTE_ON_INVALID, null);
/*
NumericEntityEscaper is deprecated starting with version 3.6 of commons-lang3, however the indicated replacement comes from
@@ -148,12 +155,10 @@ public class XSSFilterImpl implements XSSFilter {
static final Attribute DEFAULT_HREF_ATTRIBUTE = new Attribute(
"href",
Arrays.asList(
- Pattern.compile(RELATIVE_REF),
- Pattern.compile(URI)
- ),
- Collections.emptyList(),
- "removeAttribute", ""
- );
+ new Regexp("relative-ref", RELATIVE_REF),
+ new Regexp("uri", URI)),
+ null,
+ AntiSamyActions.REMOVE_ATTRIBUTE_ON_INVALID, null);
static final String DEFAULT_POLICY_PATH = "sling/xss/config.xml";
static final String EMBEDDED_POLICY_PATH = "SLING-INF/content/config.xml";
@@ -229,18 +234,17 @@ public class XSSFilterImpl implements XSSFilter {
private boolean runHrefValidation(@NotNull String url) {
// Same logic as in org.owasp.validator.html.scan.MagicSAXFilter.startElement()
- boolean isValid = hrefAttribute.containsAllowedValue(url.toLowerCase());
+ String urlLowerCase = url.toLowerCase();
+ boolean isValid = hrefAttribute.containsAllowedValue(urlLowerCase);
if (!isValid) {
try {
- isValid = hrefAttribute.matchesAllowedExpression(url.toLowerCase());
+ isValid = hrefAttribute.matchesAllowedExpression(urlLowerCase);
} catch (StackOverflowError e) {
logger.debug("Detected a StackOverflowError when validating url {} with configured regexes. Trying fallback.", url);
try {
- for (Pattern p : BACKUP_PATTERNS) {
- isValid = p.matcher(url.toLowerCase()).matches();
- if (isValid) {
- break;
- }
+ isValid = FALLBACK_HREF_ATTRIBUTE.containsAllowedValue(urlLowerCase);
+ if (!isValid) {
+ isValid = FALLBACK_HREF_ATTRIBUTE.matchesAllowedExpression(urlLowerCase);
}
} catch (StackOverflowError inner) {
logger.debug("Detected a StackOverflowError when validating url {} with fallback regexes", url);
@@ -279,30 +283,26 @@ public class XSSFilterImpl implements XSSFilter {
}
}
- private synchronized void updatePolicy() {
+ synchronized void updatePolicy() {
this.policyHandler = null;
try (final ResourceResolver xssResourceResolver = resourceResolverFactory.getServiceResourceResolver(null)) {
Resource policyResource = xssResourceResolver.getResource(policyPath);
if (policyResource != null) {
- activePolicy = new AntiSamyPolicy(false, policyResource.getPath());
- try (InputStream policyStream = activePolicy.read()) {
- setPolicyHandler(new PolicyHandler(policyStream));
- logger.info("Installed policy from {}.", activePolicy.path);
- } catch (Exception e) {
- activePolicy = null;
- Throwable[] suppressed = e.getSuppressed();
- if (suppressed.length > 0) {
- for (Throwable t : suppressed) {
- logger.error("Unable to load policy from " + policyResource.getPath(), t);
- }
- }
- logger.error("Unable to load policy from " + policyResource.getPath(), e);
- }
+ setActivePolicy(policyResource);
}
} catch (final LoginException e) {
logger.error("Unable to load the default policy file.", e);
}
if (policyHandler == null) {
+ // the content was not installed but the service is active; let's use the embedded file for the default handler
+ setActiveEmbededPolicy();
+ }
+ if (policyHandler == null || activePolicy == null) {
+ throw new IllegalStateException("Cannot load a policy handler.");
+ }
+ }
+
+ private void setActiveEmbededPolicy() {
// the content was not installed but the service is active; let's use the embedded file for the default handler
logger.info("Could not find a policy file at the configured location {}. Attempting to use the default resource embedded in" +
" the bundle.", policyPath);
@@ -320,9 +320,23 @@ public class XSSFilterImpl implements XSSFilter {
}
logger.error("Unable to load policy from embedded policy file.", e);
}
- }
- if (policyHandler == null || activePolicy == null) {
- throw new IllegalStateException("Cannot load a policy handler.");
+
+ }
+
+ private void setActivePolicy(Resource policyResource) {
+ activePolicy = new AntiSamyPolicy(false, policyResource.getPath());
+ try (InputStream policyStream = activePolicy.read()) {
+ setPolicyHandler(new PolicyHandler(policyStream));
+ logger.info("Installed policy from {}.", activePolicy.path);
+ } catch (Exception e) {
+ activePolicy = null;
+ Throwable[] suppressed = e.getSuppressed();
+ if (suppressed.length > 0) {
+ for (Throwable t : suppressed) {
+ logger.error("Unable to load policy from " + policyResource.getPath(), t);
+ }
+ }
+ logger.error("Unable to load policy from " + policyResource.getPath(), e);
}
}
@@ -341,7 +355,7 @@ public class XSSFilterImpl implements XSSFilter {
}
private void setPolicyHandler(PolicyHandler policyHandler) {
- Tag linkTag = policyHandler.getPolicy().getTagByLowercaseName("a");
+ Tag linkTag = policyHandler.getPolicy().getTagRules().get("a");
Attribute hrefAttribute = (linkTag != null) ? linkTag.getAttributeByName("href") : null;
if (hrefAttribute == null) {
// Fallback to default configuration
diff --git a/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java b/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
index d0e28f9..c136276 100644
--- a/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
+++ b/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
@@ -20,6 +20,7 @@ package org.apache.sling.xss.impl.status;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -53,7 +54,7 @@ public class XSSStatusService {
public static final int MAX_INVALID_URLS_RECORDED = 1000;
- private Map<String, AtomicInteger> invalidUrls;
+ private Map<String, AtomicInteger> invalidUrls = new HashMap<>();
public void reportInvalidUrl(@NotNull String url) {
if (invalidUrls.containsKey(url)) {
diff --git a/src/main/java/org/apache/sling/xss/impl/style/BatikCssCleaner.java b/src/main/java/org/apache/sling/xss/impl/style/BatikCssCleaner.java
new file mode 100644
index 0000000..2623723
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/style/BatikCssCleaner.java
@@ -0,0 +1,82 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.style;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.apache.batik.css.parser.Parser;
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy.CssPolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.css.sac.CSSException;
+import org.w3c.css.sac.InputSource;
+
+public class BatikCssCleaner {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private final CssPolicy cssPolicy;
+
+ private static final String CDATA_PRE = "<![CDATA[";
+ private static final String CDATA_POST = "]]>";
+
+ public BatikCssCleaner(CssPolicy cssPolicy) {
+ this.cssPolicy = cssPolicy;
+ }
+
+ /**
+ * Parses a CSS stylesheet and returns it in a safe form
+ *
+ * @param untrustedCss a complete CSS stylesheet
+ * @return the cleaned CSS stylesheet text
+ */
+ public String cleanStylesheet(String untrustedCss) {
+ try {
+ if ( untrustedCss.startsWith(CDATA_PRE) && untrustedCss.endsWith(CDATA_POST) )
+ untrustedCss = untrustedCss.substring(CDATA_PRE.length(), untrustedCss.length() - CDATA_POST.length());
+ Parser parser = new Parser();
+ ValidatingDocumentHandler handler = new ValidatingDocumentHandler(cssPolicy, false);
+ parser.setDocumentHandler(handler);
+ parser.parseStyleSheet(new InputSource(new StringReader(untrustedCss)));
+ return handler.getValidCss();
+ } catch (CSSException | IOException e) {
+ logger.warn("Unexpected error while cleaning stylesheet", e);
+ return "";
+ }
+ }
+
+ /**
+ * Parses a CSS style declaration (i.e. the text of a <tt>style</tt> attribute) and returns it in a safe form
+ *
+ * @param untrustedCss a css style declaration
+ * @return the cleaned CSS style declaration
+ */
+ public String cleanStyleDeclaration(String untrustedCss) {
+ try {
+ Parser parser = new Parser();
+ ValidatingDocumentHandler handler = new ValidatingDocumentHandler(cssPolicy, true);
+ parser.setDocumentHandler(handler);
+ parser.parseStyleDeclaration(new InputSource(new StringReader(untrustedCss)));
+ return handler.getValidCss();
+ } catch (CSSException | IOException e) {
+ logger.warn("Unexpected error while cleaning style declaration", e);
+ return "";
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/style/CssValidator.java b/src/main/java/org/apache/sling/xss/impl/style/CssValidator.java
new file mode 100644
index 0000000..f3db08d
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/style/CssValidator.java
@@ -0,0 +1,52 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.style;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy.CssPolicy;
+import org.owasp.html.AttributePolicy;
+import org.owasp.html.HtmlStreamEventProcessor;
+
+public class CssValidator {
+
+ public static final String STYLE_TAG_NAME = "style";
+ public static final String STYLE_ATTRIBUTE_NAME = STYLE_TAG_NAME;
+
+ private final BatikCssCleaner cssParser;
+ private final List<String> disallowedTagNames = new ArrayList<>();
+
+ public CssValidator(CssPolicy cssPolicy) {
+ cssParser = new BatikCssCleaner(cssPolicy);
+ }
+
+ public HtmlStreamEventProcessor newStyleTagProcessor() {
+ return new StyleTagProcessor(cssParser);
+ }
+
+ public AttributePolicy newCssAttributePolicy() {
+ return (String elementName, String attributeName, String value) -> cssParser.cleanStyleDeclaration(value);
+ }
+
+ public List<String> getDisallowedTagNames() {
+ return Collections.unmodifiableList(disallowedTagNames);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/style/StyleTagProcessor.java b/src/main/java/org/apache/sling/xss/impl/style/StyleTagProcessor.java
new file mode 100644
index 0000000..01c3e78
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/style/StyleTagProcessor.java
@@ -0,0 +1,79 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.style;
+
+import java.util.List;
+
+import org.owasp.html.HtmlStreamEventProcessor;
+import org.owasp.html.HtmlStreamEventReceiver;
+
+class StyleTagProcessor implements HtmlStreamEventProcessor {
+
+ private final BatikCssCleaner cssCleaner;
+
+ StyleTagProcessor(BatikCssCleaner cssCleaner) {
+ this.cssCleaner = cssCleaner;
+ }
+
+ @Override
+ public HtmlStreamEventReceiver wrap(HtmlStreamEventReceiver sink) {
+ return new StyleTagReceiver(sink);
+ }
+
+ class StyleTagReceiver implements HtmlStreamEventReceiver {
+
+ private final HtmlStreamEventReceiver wrapped;
+ private boolean inStyleTag;
+
+ StyleTagReceiver(HtmlStreamEventReceiver wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public void openDocument() {
+ wrapped.openDocument();
+ }
+
+ @Override
+ public void closeDocument() {
+ wrapped.closeDocument();
+ }
+
+ @Override
+ public void openTag(String elementName, List<String> attrs) {
+ wrapped.openTag(elementName, attrs);
+ inStyleTag = CssValidator.STYLE_TAG_NAME.equals(elementName);
+ }
+
+ @Override
+ public void closeTag(String elementName) {
+ wrapped.closeTag(elementName);
+ inStyleTag = false;
+ }
+
+ @Override
+ public void text(String taintedCss) {
+ if (inStyleTag ) {
+ wrapped.text(cssCleaner.cleanStylesheet(taintedCss));
+ } else {
+ wrapped.text(taintedCss);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/style/ValidatingDocumentHandler.java b/src/main/java/org/apache/sling/xss/impl/style/ValidatingDocumentHandler.java
new file mode 100644
index 0000000..ec506b8
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/style/ValidatingDocumentHandler.java
@@ -0,0 +1,342 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy.CssPolicy;
+import org.apache.sling.xss.impl.xml.Property;
+import org.w3c.css.sac.CSSException;
+import org.w3c.css.sac.CombinatorCondition;
+import org.w3c.css.sac.Condition;
+import org.w3c.css.sac.ConditionalSelector;
+import org.w3c.css.sac.DescendantSelector;
+import org.w3c.css.sac.DocumentHandler;
+import org.w3c.css.sac.InputSource;
+import org.w3c.css.sac.LexicalUnit;
+import org.w3c.css.sac.NegativeCondition;
+import org.w3c.css.sac.SACMediaList;
+import org.w3c.css.sac.Selector;
+import org.w3c.css.sac.SelectorList;
+import org.w3c.css.sac.SiblingSelector;
+
+public class ValidatingDocumentHandler implements DocumentHandler {
+
+ private final CssPolicy cssPolicy;
+ private final StringBuilder cleanCss = new StringBuilder();
+ private final boolean isInLine;
+
+ private boolean isInSelector;
+
+ public ValidatingDocumentHandler(CssPolicy cssPolicy, boolean isInLine) {
+ this.cssPolicy = cssPolicy;
+ this.isInLine = isInLine;
+ }
+
+ @Override
+ public void startSelector(SelectorList selectors) throws CSSException {
+
+ List<String> validSelectors = validateSelectors(selectors);
+ if ( validSelectors.isEmpty() )
+ return;
+
+ StringJoiner joiner = new StringJoiner(", ", "", " {\n");
+ validSelectors.forEach( joiner::add );
+ cleanCss.append(joiner.toString());
+ isInSelector = true;
+ }
+
+ @Override
+ public void endSelector(SelectorList selectors) throws CSSException {
+ if ( !isInSelector )
+ return;
+
+ cleanCss.append("}\n");
+ isInSelector = false;
+ }
+
+ @Override
+ public void property(String name, LexicalUnit value, boolean important) throws CSSException {
+ if (!isInSelector && !isInLine) {
+ return;
+ }
+
+ List<String> validPropertyValues = validatePropertyValues(name, value);
+ if ( validPropertyValues.isEmpty() )
+ return;
+
+ cleanCss.append(validPropertyValues.stream()
+ .map( s -> important ? s + " !important" : s)
+ .collect(Collectors.joining(" ", "\t" + name + ": ", ";\n")));
+ }
+
+ private List<String> validateSelectors(SelectorList selectors) {
+ List<String> selectorNames = new ArrayList<>();
+ for ( int i = 0 ; i < selectors.getLength(); i++ ) {
+ Selector selector = selectors.item(i);
+ if ( isValidSelector(selector) )
+ selectorNames.add(selector.toString());
+ }
+ return selectorNames;
+ }
+
+ private boolean isValidSelector(Selector selector) {
+ switch ( selector.getSelectorType() ) {
+ case Selector.SAC_ANY_NODE_SELECTOR:
+ case Selector.SAC_ELEMENT_NODE_SELECTOR:
+ case Selector.SAC_PSEUDO_ELEMENT_SELECTOR:
+ case Selector.SAC_ROOT_NODE_SELECTOR:
+ case Selector.SAC_NEGATIVE_SELECTOR:
+ return cssPolicy.isValidElementName(selector.toString().toLowerCase(Locale.ENGLISH));
+ case Selector.SAC_DIRECT_ADJACENT_SELECTOR:
+ SiblingSelector sibling = (SiblingSelector) selector;
+ return isValidSelector(sibling.getSiblingSelector()) && isValidSelector(sibling.getSelector());
+ case Selector.SAC_CONDITIONAL_SELECTOR:
+ ConditionalSelector conditional = (ConditionalSelector) selector;
+ return isValidSelector(conditional.getSimpleSelector()) && isValidCondition(conditional.getCondition());
+ case Selector.SAC_CHILD_SELECTOR:
+ case Selector.SAC_DESCENDANT_SELECTOR:
+ DescendantSelector descendant = (DescendantSelector) selector;
+ return isValidSelector(descendant.getAncestorSelector()) && isValidSelector(descendant.getSimpleSelector());
+ default:
+ return false;
+ }
+ }
+
+ private boolean isValidCondition(Condition condition) {
+
+ switch (condition.getConditionType()) {
+ case Condition.SAC_CLASS_CONDITION:
+ return cssPolicy.isValidClassName(condition.toString().toLowerCase(Locale.ENGLISH));
+ case Condition.SAC_ID_CONDITION:
+ return cssPolicy.isValidId(condition.toString().toLowerCase(Locale.ENGLISH));
+ case Condition.SAC_AND_CONDITION:
+ case Condition.SAC_OR_CONDITION:
+ CombinatorCondition comb = (CombinatorCondition) condition;
+ return isValidCondition(comb.getFirstCondition()) && isValidCondition(comb.getSecondCondition());
+ case Condition.SAC_NEGATIVE_CONDITION:
+ return isValidCondition(((NegativeCondition) condition).getCondition());
+ case Condition.SAC_PSEUDO_CLASS_CONDITION:
+ return cssPolicy.isValidPseudoElementName(condition.toString().toLowerCase(Locale.ENGLISH));
+ case Condition.SAC_ATTRIBUTE_CONDITION:
+ case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
+ case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
+ return false;
+ case Condition.SAC_ONLY_CHILD_CONDITION:
+ case Condition.SAC_ONLY_TYPE_CONDITION:
+ // constant values, unconditionally true
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private List<String> validatePropertyValues(String name, LexicalUnit value) {
+ List<String> validPropertyValues = new ArrayList<>();
+ while ( value != null ) {
+ String stringValue = lexicalValueToString(value);
+ value = value.getNextLexicalUnit();
+ boolean isValid = validateProperty(name, stringValue);
+ if ( !isValid )
+ continue;
+ validPropertyValues.add(stringValue);
+ }
+ return validPropertyValues;
+ }
+
+ public String getValidCss() {
+ return cleanCss.toString();
+ }
+
+ private boolean validateProperty(String name, String lexicalValueToString) {
+ if ( lexicalValueToString == null )
+ return false;
+
+ Property property = cssPolicy.getCssRules().get(name);
+ if ( property == null )
+ return false;
+
+ if ( property.getLiterals().contains(lexicalValueToString) )
+ return true;
+
+ if ( property.getRegexps().stream()
+ .anyMatch( p -> p.matcher(lexicalValueToString).matches() ) )
+ return true;
+
+ if ( property.getShorthands().stream()
+ .anyMatch( s -> validateProperty(s, lexicalValueToString)) )
+ return true;
+
+ return false;
+ }
+
+ private String lexicalValueToString(LexicalUnit lu) {
+ switch (lu.getLexicalUnitType()) {
+ case LexicalUnit.SAC_PERCENTAGE:
+ case LexicalUnit.SAC_DIMENSION:
+ case LexicalUnit.SAC_EM:
+ case LexicalUnit.SAC_EX:
+ case LexicalUnit.SAC_PIXEL:
+ case LexicalUnit.SAC_INCH:
+ case LexicalUnit.SAC_CENTIMETER:
+ case LexicalUnit.SAC_MILLIMETER:
+ case LexicalUnit.SAC_POINT:
+ case LexicalUnit.SAC_PICA:
+ case LexicalUnit.SAC_DEGREE:
+ case LexicalUnit.SAC_GRADIAN:
+ case LexicalUnit.SAC_RADIAN:
+ case LexicalUnit.SAC_MILLISECOND:
+ case LexicalUnit.SAC_SECOND:
+ case LexicalUnit.SAC_HERTZ:
+ case LexicalUnit.SAC_KILOHERTZ:
+ // various measurements
+ return lu.getFloatValue() + lu.getDimensionUnitText();
+ case LexicalUnit.SAC_INTEGER:
+ // number
+ return String.valueOf(lu.getIntegerValue());
+ case LexicalUnit.SAC_REAL:
+ // number
+ return String.valueOf(lu.getFloatValue());
+ case LexicalUnit.SAC_STRING_VALUE:
+ case LexicalUnit.SAC_IDENT:
+ // identifier, potentially needs quoting
+ String stringValue = lu.getStringValue();
+ if (stringValue.indexOf(" ") != -1)
+ stringValue = "\"" + stringValue + "\"";
+ return stringValue;
+ case LexicalUnit.SAC_URI:
+ // this is a URL
+ return "url(" + lu.getStringValue() + ")";
+ case LexicalUnit.SAC_RGBCOLOR:
+ // this is a rgb encoded color; technically we don't need to encode
+ // it precisely like this but it makes it simpler to keep the tests
+ // based on the AntiSamy implementation
+ return toRgbString(lu);
+ case LexicalUnit.SAC_INHERIT:
+ // constant
+ return "inherit";
+ case LexicalUnit.SAC_OPERATOR_COMMA:
+ return ",";
+ case LexicalUnit.SAC_ATTR:
+ case LexicalUnit.SAC_COUNTER_FUNCTION:
+ case LexicalUnit.SAC_COUNTERS_FUNCTION:
+ case LexicalUnit.SAC_FUNCTION:
+ case LexicalUnit.SAC_RECT_FUNCTION:
+ case LexicalUnit.SAC_SUB_EXPRESSION:
+ case LexicalUnit.SAC_UNICODERANGE:
+ default:
+ // unsupported
+ return null;
+ }
+ }
+
+ private String toRgbString(LexicalUnit lu) {
+ // 16 default capacity actually fits nicely
+ //
+ // rgb(255,255,255)
+ // ....|....|....|.
+ StringBuilder sb = new StringBuilder();
+ LexicalUnit param = lu.getParameters();
+ sb.append("rgb(");
+ sb.append(param.getIntegerValue()); // R value
+ sb.append(',');
+ param = param.getNextLexicalUnit(); // comma
+ param = param.getNextLexicalUnit(); // G value
+ sb.append(param.getIntegerValue());
+ sb.append(',');
+ param = param.getNextLexicalUnit(); // comma
+ param = param.getNextLexicalUnit(); // B value
+ sb.append(param.getIntegerValue());
+ sb.append(')');
+
+ return sb.toString();
+ }
+
+ @Override
+ public void importStyle(String uri, SACMediaList media, String defaultNamespaceURI) throws CSSException {
+ // embedded stylesheets are not supported
+ }
+
+ @Override
+ public void startDocument(InputSource source) throws CSSException {
+ // nothing to do
+ }
+
+ @Override
+ public void endDocument(InputSource source) throws CSSException {
+ // nothing to do
+ }
+
+ @Override
+ public void comment(String text) throws CSSException {
+ // we intentionally ignore comments, they don't need to be in the output
+ }
+
+ @Override
+ public void ignorableAtRule(String atRule) throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void namespaceDeclaration(String prefix, String uri) throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void startMedia(SACMediaList media) throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void endMedia(SACMediaList media) throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void startPage(String name, String pseudo_page) throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void endPage(String name, String pseudo_page) throws CSSException {
+ // nothing to do
+ }
+
+ @Override
+ public void startFontFace() throws CSSException {
+ // nothing to do
+
+ }
+
+ @Override
+ public void endFontFace() throws CSSException {
+ // nothing to do
+
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/AllowedEmptyTags.java b/src/main/java/org/apache/sling/xss/impl/xml/AllowedEmptyTags.java
new file mode 100644
index 0000000..15e9441
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/AllowedEmptyTags.java
@@ -0,0 +1,44 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class AllowedEmptyTags {
+
+ @JacksonXmlElementWrapper(localName = "literal-list")
+ @JacksonXmlProperty(localName = "literal")
+ private List<Literal> allowedEmptyTagsList = Collections.emptyList();
+
+ public List<Literal> getLiteralList() {
+ return allowedEmptyTagsList;
+ }
+
+ public List<String> getLiterals() {
+ // reads out the literals and creates a list out of it
+ return allowedEmptyTagsList.stream().map(Literal::getValue)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyPolicy.java b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyPolicy.java
new file mode 100644
index 0000000..da1c91a
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyPolicy.java
@@ -0,0 +1,141 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.sling.xss.impl.InvalidConfigException;
+
+public class AntiSamyPolicy {
+
+ protected final Map<String, Pattern> commonRegularExpressions = new HashMap<>();
+ protected final Map<String, Attribute> commonAttributes = new HashMap<>();
+ protected final Map<String, Tag> tagRules = new HashMap<>();
+ protected final Map<String, Property> cssRules = new HashMap<>();
+ protected final Map<String, String> directives = new HashMap<>();
+ protected final Map<String, Attribute> globalAttributes = new HashMap<>();
+ protected final Map<String, Attribute> dynamicAttributes = new HashMap<>();
+ protected final List<String> requireClosingTags = new ArrayList<>();
+ protected List<String> allowedEmptyTags = new ArrayList<>();
+
+ public AntiSamyPolicy(InputStream input) throws InvalidConfigException, XMLStreamException, IOException {
+ AntiSamyXmlParser xmlParser = new AntiSamyXmlParser();
+ MapBuilder mapBuilder = new MapBuilder();
+ AntiSamyRules root = xmlParser.createRules(input);
+ mapBuilder.createRulesMap(this, root);
+ }
+
+ public Map<String, String> getDirectives() {
+ return directives;
+ }
+
+ public List<String> getRequireClosingTags() {
+ return requireClosingTags;
+ }
+
+ public Map<String, Pattern> getCommonRegularExpressions() {
+ return commonRegularExpressions;
+ }
+
+ public Map<String, Attribute> getGlobalAttributes() {
+ return globalAttributes;
+ }
+
+ public Map<String, Attribute> getCommonAttributes() {
+ return commonAttributes;
+ }
+
+ public Map<String, Property> getCssRules() {
+ return cssRules;
+ }
+
+ public List<String> getAllowedEmptyTags() {
+ return allowedEmptyTags;
+ }
+
+ public Map<String, Tag> getTagRules() {
+ return tagRules;
+ }
+
+ public Map<String, Attribute> getDynamicAttributes() {
+ return dynamicAttributes;
+ }
+
+ public CssPolicy getCssPolicy() {
+ return new CssPolicy(cssRules,
+ commonRegularExpressions);
+ }
+
+ public static class CssPolicy {
+
+ private final Map<String, Property> cssRules;
+ private final IncludeExcludeMatcher elementMatcher;
+ private final IncludeExcludeMatcher classMatcher;
+ private final IncludeExcludeMatcher idMatcher;
+ private final IncludeExcludeMatcher pseudoElementMatcher;
+ private final IncludeExcludeMatcher attributeMatcher;
+
+ public CssPolicy(Map<String, Property> cssrules, Map<String, Pattern> commonRegExps) {
+ this.cssRules = Collections.unmodifiableMap(cssrules);
+ this.elementMatcher = new IncludeExcludeMatcher(commonRegExps.get("cssElementSelector"),
+ commonRegExps.get("cssElementExclusion"));
+ this.classMatcher = new IncludeExcludeMatcher(commonRegExps.get("cssClassSelector"),
+ commonRegExps.get("cssClassExclusion"));
+ this.idMatcher = new IncludeExcludeMatcher(commonRegExps.get("cssIDSelector"),
+ commonRegExps.get("cssIDExclusion"));
+ this.pseudoElementMatcher = new IncludeExcludeMatcher(commonRegExps.get("cssPseudoElementSelector"),
+ commonRegExps.get("cssPseudoElementExclusion"));
+ this.attributeMatcher = new IncludeExcludeMatcher(commonRegExps.get("cssAttributeSelector"),
+ commonRegExps.get("cssAttributeExclusion"));
+ }
+
+ public Map<String, Property> getCssRules() {
+ return cssRules;
+ }
+
+ public boolean isValidElementName(String name) {
+ return elementMatcher.matches(name);
+ }
+
+ public boolean isValidClassName(String name) {
+ return classMatcher.matches(name);
+ }
+
+ public boolean isValidId(String name) {
+ return idMatcher.matches(name);
+ }
+
+ public boolean isValidPseudoElementName(String name) {
+ return pseudoElementMatcher.matches(name);
+ }
+
+ public boolean isValidAttributeSelector(String name) {
+ return attributeMatcher.matches(name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyRules.java b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyRules.java
new file mode 100644
index 0000000..3e830a7
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyRules.java
@@ -0,0 +1,127 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+
+@JacksonXmlRootElement(localName = "anti-samy-rules")
+public class AntiSamyRules {
+ @JsonIgnore
+ @JacksonXmlProperty(localName = "xmlns:xsi", isAttribute = true)
+ private String xmlnsXsi;
+
+ @JsonIgnore
+ @JacksonXmlProperty(localName = "noNamespaceSchemaLocation", isAttribute = true)
+ private String noNamespaceSchemaLocation;
+
+ @JacksonXmlElementWrapper(localName = "directives")
+ @JacksonXmlProperty(localName = "directive")
+ private List<Directive> directiveList = Collections.emptyList();
+
+ @JacksonXmlElementWrapper(localName = "common-regexps")
+ @JacksonXmlProperty(localName = "regexp")
+ private List<Regexp> regexpList = Collections.emptyList();
+
+ @JacksonXmlElementWrapper(localName = "common-attributes")
+ @JacksonXmlProperty(localName = "attribute")
+ private List<Attribute> commonAttributeList = Collections.emptyList();
+
+ @JacksonXmlProperty(localName = "global-tag-attributes")
+ private GlobalTagAttributes globalTagAttributes;
+
+ @JacksonXmlProperty(localName = "dynamic-tag-attributes")
+ private DynamicTagAttributes dynamicTagAttribute;
+
+ @JacksonXmlElementWrapper(localName = "tag-rules")
+ @JacksonXmlProperty(localName = "tag")
+ private List<Tag> tagRulesList = Collections.emptyList();
+
+ @JacksonXmlProperty(localName = "tags-to-encode")
+ private TagsToEncode tagsToEncode;
+
+ @JacksonXmlElementWrapper(localName = "css-rules")
+ @JacksonXmlProperty(localName = "property")
+ private List<Property> propertyList = Collections.emptyList();
+
+ @JacksonXmlProperty(localName = "allowed-empty-tags")
+ private AllowedEmptyTags allowedEmptyTags;
+
+ public AllowedEmptyTags getAllowedEmptyTags() {
+ return allowedEmptyTags;
+ }
+
+ public DynamicTagAttributes getDynamicTagAttribute() {
+ return dynamicTagAttribute;
+ }
+
+ public GlobalTagAttributes getGlobalTagAttributes() {
+ return globalTagAttributes;
+ }
+
+ public String getNoNamespaceSchemaLocation() {
+ return noNamespaceSchemaLocation;
+ }
+
+ public TagsToEncode getTagsToEncode() {
+ return tagsToEncode;
+ }
+
+ public String getXmlnsXsi() {
+ return xmlnsXsi;
+ }
+
+ public List<Attribute> getCommonAttributeList() {
+ return commonAttributeList;
+ }
+
+ public List<Directive> getDirectiveList() {
+ return directiveList;
+ }
+
+ public List<Regexp> getRegexpList() {
+ return regexpList;
+ }
+
+ public List<Property> getPropertyList() {
+ return propertyList;
+ }
+
+ public List<Tag> getTagRulesList() {
+ return tagRulesList;
+ }
+
+ public Map<String, String> getDirectivesByName() {
+ return directiveList.stream()
+ .collect(Collectors.toMap(Directive::getName, Directive::getValue));
+ }
+
+ public Map<String, Pattern> getCommonPatternByName() {
+ return regexpList.stream()
+ .collect(Collectors.toMap(Regexp::getName, Regexp::getPattern));
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyXmlParser.java b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyXmlParser.java
new file mode 100644
index 0000000..e871121
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/AntiSamyXmlParser.java
@@ -0,0 +1,56 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+public class AntiSamyXmlParser {
+
+ private static final String DIRECTIVE_EMBED_STYLE_SHEETS = "embedStyleSheets";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public AntiSamyRules createRules(InputStream input) throws XMLStreamException, IOException {
+
+ XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
+ // disable external entities declarations
+ xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+
+ XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(input);
+ XmlMapper mapper = new XmlMapper(xmlInputFactory, XMLOutputFactory.newInstance());
+ AntiSamyRules rules = mapper.readValue(xmlStreamReader, AntiSamyRules.class);
+ if ("true".equals(rules.getDirectivesByName().get(DIRECTIVE_EMBED_STYLE_SHEETS))) {
+ logger.warn("Unsupported configuration directive {} is set to true and will be ignored",
+ DIRECTIVE_EMBED_STYLE_SHEETS);
+ }
+ xmlStreamReader.close();
+ return rules;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Attribute.java b/src/main/java/org/apache/sling/xss/impl/xml/Attribute.java
new file mode 100644
index 0000000..c1a14eb
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Attribute.java
@@ -0,0 +1,139 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.sling.xss.impl.AntiSamyActions;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import org.jetbrains.annotations.NotNull;
+
+public class Attribute {
+
+ @NotNull
+ private final String name;
+
+ private final String description;
+ private final String onInvalid;
+ private final List<Regexp> regexpList;
+ private final List<Literal> literalList;
+
+ @JsonCreator
+ public Attribute(@JacksonXmlProperty(localName = "name", isAttribute = true) @NotNull String name,
+ @JacksonXmlProperty(localName = "regexp-list") List<Regexp> allowedRegexps,
+ @JacksonXmlProperty(localName = "literal-list") List<Literal> literalList,
+ @JacksonXmlProperty(localName = "onInvalid", isAttribute = true) String onInvalid,
+ @JacksonXmlProperty(localName = "description", isAttribute = true) String description) {
+ this.name = name;
+ this.description = Optional.ofNullable(description).orElse("");
+ this.onInvalid = onInvalid != null && onInvalid.length() > 0 ? onInvalid : AntiSamyActions.REMOVE_ATTRIBUTE_ON_INVALID;
+ this.regexpList = Optional.ofNullable(allowedRegexps)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.literalList = Optional.ofNullable(literalList)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ }
+
+ @Override
+ public String toString() {
+ return "Attribute - name: " + name + ", description " + description + ", onInvalid " + onInvalid
+ + ", allowedRegexlist: "
+ + regexpList.size() + ", literals " + literalList;
+ }
+
+ @NotNull
+ public String getOnInvalid() {
+ return onInvalid;
+ }
+
+ @NotNull
+ public String getDescription() {
+ return description;
+ }
+
+ @NotNull
+ public String getName() {
+ return name;
+ }
+
+ @NotNull
+ public List<String> getLiterals() {
+ return getLiteralList().stream()
+ .map(Literal::getValue)
+ .map(String::toLowerCase)
+ .collect(Collectors.toList());
+ }
+
+ @NotNull
+ public List<Literal> getLiteralList() {
+ return literalList;
+ }
+
+ @NotNull
+ public List<Pattern> getPatternList() {
+ return getRegexpList().stream()
+ .map(Regexp::getPattern)
+ .collect(Collectors.toList());
+ }
+
+ @NotNull
+ public List<Regexp> getRegexpList() {
+ return regexpList;
+ }
+
+ public boolean containsAllowedValue(String valueInLowerCase) {
+ return getLiteralList().stream()
+ .map(Literal::getValue)
+ .anyMatch(valueInLowerCase::equals);
+ }
+
+ public boolean matchesAllowedExpression(String value) {
+ return getPatternList().stream()
+ .anyMatch(pattern -> pattern.matcher(value).matches());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ if (obj instanceof Attribute) {
+ Attribute attribute = (Attribute) obj;
+ return name == attribute.name || attribute.name.equals(name)
+ && description == attribute.description || attribute.description.equals(description)
+ && onInvalid == attribute.onInvalid
+ || attribute.onInvalid.equals(onInvalid)
+ && regexpList == attribute.regexpList
+ || attribute.regexpList.equals(regexpList)
+ && literalList == attribute.literalList;
+
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() + description.hashCode() + onInvalid.hashCode();
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Category.java b/src/main/java/org/apache/sling/xss/impl/xml/Category.java
new file mode 100644
index 0000000..68f55f0
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Category.java
@@ -0,0 +1,30 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Category {
+ @JacksonXmlProperty(isAttribute = true)
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Directive.java b/src/main/java/org/apache/sling/xss/impl/xml/Directive.java
new file mode 100644
index 0000000..a1aafd4
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Directive.java
@@ -0,0 +1,37 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Directive {
+ @JacksonXmlProperty(isAttribute = true)
+ private String name;
+ @JacksonXmlProperty(isAttribute = true)
+ private String value;
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/DynamicTagAttributes.java b/src/main/java/org/apache/sling/xss/impl/xml/DynamicTagAttributes.java
new file mode 100644
index 0000000..33e46ec
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/DynamicTagAttributes.java
@@ -0,0 +1,36 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class DynamicTagAttributes {
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "attribute")
+ private List<Attribute> dynamicTagAttributeList = Collections.emptyList();
+
+ public List<Attribute> getDynamicTagAttributeList() {
+ return dynamicTagAttributeList;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/GlobalTagAttributes.java b/src/main/java/org/apache/sling/xss/impl/xml/GlobalTagAttributes.java
new file mode 100644
index 0000000..5aeac49
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/GlobalTagAttributes.java
@@ -0,0 +1,36 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class GlobalTagAttributes {
+
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "attribute")
+ private List<Attribute> globalTagAttributesList = Collections.emptyList();
+
+ public List<Attribute> getGlobalTagAttributeList() {
+ return globalTagAttributesList;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/IncludeExcludeMatcher.java b/src/main/java/org/apache/sling/xss/impl/xml/IncludeExcludeMatcher.java
new file mode 100644
index 0000000..da25d5e
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/IncludeExcludeMatcher.java
@@ -0,0 +1,41 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.regex.Pattern;
+
+class IncludeExcludeMatcher {
+ private final Pattern include;
+ private final Pattern exclude;
+
+ public IncludeExcludeMatcher(Pattern include, Pattern exclude) {
+ this.include = include;
+ this.exclude = exclude;
+ }
+
+ public boolean matches(String input) {
+ if ( !include.matcher(input).matches() )
+ return false;
+
+ if ( exclude == null )
+ return true;
+
+ return ! exclude.matcher(input).matches();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Literal.java b/src/main/java/org/apache/sling/xss/impl/xml/Literal.java
new file mode 100644
index 0000000..277569b
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Literal.java
@@ -0,0 +1,43 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Literal {
+ @JacksonXmlProperty(isAttribute = true, localName = "value")
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Literal) {
+ return ((Literal) obj).value == value || ((Literal) obj).value.equals(value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/MapBuilder.java b/src/main/java/org/apache/sling/xss/impl/xml/MapBuilder.java
new file mode 100644
index 0000000..734c363
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/MapBuilder.java
@@ -0,0 +1,246 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.sling.xss.impl.InvalidConfigException;
+
+class MapBuilder {
+
+ AntiSamyPolicy policy;
+ // Antisamy hardcodes the allowed-empty-tags default:
+ // https://github.com/nahsra/antisamy/blob/main/src/main/java/org/owasp/validator/html/scan/Constants.java#L37
+ private static final List<String> ALLOWED_EMPTY_TAGS = Arrays.asList(
+ "br",
+ "hr",
+ "a",
+ "img",
+ "link",
+ "iframe",
+ "script",
+ "object",
+ "applet",
+ "frame",
+ "base",
+ "param",
+ "meta",
+ "input",
+ "textarea",
+ "embed",
+ "basefont",
+ "col");
+
+ public void createRulesMap(AntiSamyPolicy policy, AntiSamyRules topLevelElement) throws InvalidConfigException {
+ this.policy = policy;
+
+ parseCommonRegExps(topLevelElement.getRegexpList());
+ parseDirectives(topLevelElement.getDirectiveList());
+ parseAllowedEmptyTags(topLevelElement.getAllowedEmptyTags());
+ parseCommonAttributes(topLevelElement.getCommonAttributeList());
+ parseGlobalAttributes(topLevelElement.getGlobalTagAttributes().getGlobalTagAttributeList());
+ parseDynamicAttributes(topLevelElement.getDynamicTagAttribute().getDynamicTagAttributeList());
+ parseTagRules(topLevelElement.getTagRulesList());
+
+ parseCSSRules(topLevelElement.getPropertyList());
+ }
+
+ /**
+ * Go through the <common-regexps> section of the policy file.
+ *
+ * @param root Top level of <common-regexps>
+ */
+ private void parseCommonRegExps(List<Regexp> root) {
+ for (Regexp regex : root) {
+ String name = regex.getName();
+ Pattern regexp = Pattern.compile(regex.getValue(),
+ Pattern.DOTALL);
+ policy.commonRegularExpressions.put(name, regexp);
+ }
+ }
+
+ /**
+ * Go through <directives> section of the policy file.
+ *
+ * @param root Top level of <directives>
+ */
+ private void parseDirectives(List<Directive> root) {
+ for (Directive directive : root) {
+ String name = directive.getName();
+ String value = directive.getValue();
+ policy.directives.put(name, value);
+ }
+ }
+
+ private void parseCommonAttributes(List<Attribute> root) {
+ for (Attribute attribute : root) {
+ List<Regexp> allowedRegexps = getAllowedRegexps(attribute.getRegexpList());
+ Attribute newAttribute = new Attribute(attribute.getName(), allowedRegexps, attribute.getLiteralList(),
+ attribute.getOnInvalid(), attribute.getDescription());
+ policy.commonAttributes.put(attribute.getName().toLowerCase(), newAttribute);
+ }
+ }
+
+ /**
+ * Go through <allowed-empty-tags> section of the policy file.
+ *
+ * @param allowedEmptyTagsListNode Top level of <allowed-empty-tags>
+ * @param allowedEmptyTags The tags that can be empty
+ */
+ private void parseAllowedEmptyTags(AllowedEmptyTags allowedEmptyTagsList) {
+ if (allowedEmptyTagsList != null) {
+ policy.allowedEmptyTags = allowedEmptyTagsList.getLiterals();
+ } else
+ policy.allowedEmptyTags.addAll(ALLOWED_EMPTY_TAGS);
+ }
+
+ /**
+ * Go through <global-tag-attributes> section of the policy file.
+ *
+ * @param root Top level of <global-tag-attributes>
+ * @param globalAttributes1 A HashMap of global Attributes that need
+ validation
+ * for every tag.
+ * @param commonAttributes The common attributes
+ * @throws InvalidConfigException
+ */
+ private void parseGlobalAttributes(List<Attribute> root) throws InvalidConfigException {
+ for (Attribute ele : root) {
+ String name = ele.getName();
+ Attribute toAdd = policy.commonAttributes.get(name.toLowerCase());
+
+ if (toAdd != null)
+ policy.globalAttributes.put(name.toLowerCase(), toAdd);
+ else
+ throw new InvalidConfigException("Global attribute '" + name
+ + "' was not defined in <common-attributes>");
+ }
+ }
+
+ /**
+ * Go through <dynamic-tag-attributes> section of the policy file.
+ *
+ * @param root Top level of <dynamic-tag-attributes>
+ * @param dynamicAttributes A HashMap of dynamic Attributes that need
+ validation
+ * for every tag.
+ * @param commonAttributes The common attributes
+ * @throws InvalidConfigException
+ */
+
+ private void parseDynamicAttributes(List<Attribute> root) throws InvalidConfigException {
+ for (Attribute ele : root) {
+ String name = ele.getName();
+ Attribute toAdd = policy.getCommonAttributes().get(name.toLowerCase());
+
+ if (toAdd != null) {
+ String attrName = name.toLowerCase().substring(0, name.length() - 1);
+ policy.getDynamicAttributes().put(attrName, toAdd);
+ } else
+ throw new InvalidConfigException("Dynamic attribute '" + name
+ + "' was not defined in <common-attributes>");
+ }
+ }
+
+ private void parseTagRules(List<Tag> root) throws InvalidConfigException {
+ if (root == null)
+ return;
+
+ for (Tag tagNode : root) {
+ String name = tagNode.getName();
+ String action = tagNode.getAction();
+
+ List<Attribute> attributeList = tagNode.getAttributeList();
+ List<Attribute> tagAttributes = getTagAttributes(attributeList, name);
+ Tag tag = new Tag(name, action, tagAttributes);
+
+ policy.tagRules.put(name.toLowerCase(), tag);
+ }
+ }
+
+ private List<Attribute> getTagAttributes(List<Attribute> attributeList, String tagName)
+ throws InvalidConfigException {
+ List<Attribute> tagAttributes = new ArrayList<>();
+
+ for (Attribute attribute : attributeList) {
+ Attribute newAttribute;
+ String attributeName = attribute.getName().toLowerCase();
+ List<Regexp> regexps = attribute.getRegexpList();
+ List<Literal> literals = attribute.getLiteralList();
+ String onInvalid = attribute.getOnInvalid();
+ String description = attribute.getDescription();
+
+ // attribute has no children
+ if (regexps.isEmpty() && literals.isEmpty()) {
+ Attribute commonAttribute = policy.commonAttributes.get(attributeName);
+ if (commonAttribute != null) {
+ // creates a new Attribute with the fetched Attribute's information if not
+ // available
+ newAttribute = new Attribute(attributeName,
+ !regexps.isEmpty() ? regexps : commonAttribute.getRegexpList(),
+ !literals.isEmpty() ? literals : commonAttribute.getLiteralList(),
+ !onInvalid.isEmpty() ? onInvalid : commonAttribute.getOnInvalid(),
+ !description.isEmpty() ? description : commonAttribute.getDescription());
+ } else {
+ throw new InvalidConfigException("Attribute '" + attributeName +
+ "' was referenced as a common attribute in definition of '" + tagName +
+ "', but does not exist in <common-attributes>");
+ }
+ } else {
+ List<Regexp> commonAllowedRegexps = getAllowedRegexps(regexps);
+ List<Literal> allowedValues = attribute.getLiteralList();
+ newAttribute = new Attribute(attributeName, commonAllowedRegexps, allowedValues, onInvalid,
+ description);
+ }
+ // Add fully built attribute.
+ tagAttributes.add(newAttribute);
+ }
+ return tagAttributes;
+ }
+
+ private void parseCSSRules(List<Property> root) {
+
+ for (Property property : root) {
+ List<Regexp> allowedRegexp3 = getAllowedRegexps(property.getRegexpList());
+ Property propertyWithPatterns = new Property(property.getName(), allowedRegexp3, property.getLiteralList(),
+ property.getShorthandList(), property.getDescription(), property.getOnInvalid(),
+ property.getDefaultValue());
+ policy.getCssRules().put(property.getName().toLowerCase(), propertyWithPatterns);
+ }
+ }
+
+ private List<Regexp> getAllowedRegexps(List<Regexp> nameAndRegexpsList) {
+ List<Regexp> allowedRegExp = new ArrayList<>();
+ for (Regexp regExpNode : nameAndRegexpsList) {
+ String regExpName = regExpNode.getName();
+ String value = regExpNode.getValue();
+
+ if (regExpName != null && regExpName.length() > 0) {
+ allowedRegExp
+ .add(new Regexp(regExpName, policy.getCommonRegularExpressions().get(regExpName).toString()));
+ } else if (value != null) {
+ allowedRegExp.add(new Regexp(regExpName, value));
+ }
+ }
+ return allowedRegExp;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Property.java b/src/main/java/org/apache/sling/xss/impl/xml/Property.java
new file mode 100644
index 0000000..94accb4
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Property.java
@@ -0,0 +1,125 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.sling.xss.impl.AntiSamyActions;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Property {
+ private String name;
+ private String description;
+ private String defaultValue;
+
+ @JacksonXmlElementWrapper(localName = "regexp-list")
+ private List<Regexp> regexpList;
+
+ @JacksonXmlElementWrapper(localName = "literal-list")
+ private List<Literal> literalList;
+
+ @JacksonXmlElementWrapper(localName = "category-list")
+ private List<Category> categoryList;
+
+ @JacksonXmlElementWrapper(localName = "shorthand-list")
+ private List<Shorthand> shorthandList;
+
+ private String onInvalid;
+
+ @JsonCreator
+ public Property(@JacksonXmlProperty(localName = "name", isAttribute = true) String name,
+ @JacksonXmlProperty(localName = "regexp") List<Regexp> allowedRegexps,
+ @JacksonXmlProperty(localName = "literal") List<Literal> allowedValue,
+ @JacksonXmlProperty(localName = "shorthand") List<Shorthand> shortHandRefs,
+ @JacksonXmlProperty(localName = "description", isAttribute = true) String description,
+ @JacksonXmlProperty(localName = "onInvalid", isAttribute = true) String onInvalidStr,
+ @JacksonXmlProperty(isAttribute = true, localName = "default") String defaultValue) {
+
+ this.name = name;
+ this.description = Optional.ofNullable(description).orElse("");
+ this.onInvalid = onInvalid != null && onInvalid.length() > 0 ? onInvalid : AntiSamyActions.REMOVE_ATTRIBUTE_ON_INVALID;
+ this.regexpList = Optional.ofNullable(allowedRegexps)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.literalList = Optional.ofNullable(literalList)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.shorthandList = Optional.ofNullable(shortHandRefs)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.defaultValue = defaultValue;
+ }
+
+ public List<Category> getCategoryList() {
+ return categoryList;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public List<Literal> getLiteralList() {
+ return literalList;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<Regexp> getRegexpList() {
+ return regexpList;
+ }
+
+ public List<Shorthand> getShorthandList() {
+ return shorthandList;
+ }
+
+ public List<String> getShorthands() {
+ // reads out the shorthands and creates a list out of it
+ return shorthandList.stream().map(Shorthand::getName)
+ .collect(Collectors.toList());
+ }
+
+ public List<String> getLiterals() {
+ // reads out the literals and creates a list out of it
+ return literalList.stream().map(Literal::getValue)
+ .collect(Collectors.toList());
+ }
+
+ public String getOnInvalid() {
+ return onInvalid;
+ }
+
+ public List<Pattern> getRegexps() {
+ // reads out the patterns and creates a list out of it
+ return regexpList.stream().map(Regexp::getPattern)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Regexp.java b/src/main/java/org/apache/sling/xss/impl/xml/Regexp.java
new file mode 100644
index 0000000..2658aa4
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Regexp.java
@@ -0,0 +1,64 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Regexp {
+ private String name;
+ private String value;
+
+ @JsonCreator
+ public Regexp(@JacksonXmlProperty(localName = "name", isAttribute = true) String name,
+ @JacksonXmlProperty(localName = "value", isAttribute = true) String regexp) {
+
+ this.name = name;
+ this.value = regexp;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Pattern getPattern() {
+ return Pattern.compile(value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Regexp) {
+ return ((Regexp) obj).name == name || ((Regexp) obj).name.equals(name)
+ && ((Regexp) obj).value == value
+ || ((Regexp) obj).value.equals(value);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() + value.hashCode();
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Shorthand.java b/src/main/java/org/apache/sling/xss/impl/xml/Shorthand.java
new file mode 100644
index 0000000..bcbf731
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Shorthand.java
@@ -0,0 +1,30 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Shorthand {
+ @JacksonXmlProperty(isAttribute = true)
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/Tag.java b/src/main/java/org/apache/sling/xss/impl/xml/Tag.java
new file mode 100644
index 0000000..a3e015f
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/Tag.java
@@ -0,0 +1,85 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Tag {
+ private String name;
+ private String action;
+ private List<Attribute> attributeList;
+
+ @JsonCreator
+ public Tag(
+ @JacksonXmlProperty(isAttribute = true, localName = "name") String name,
+ @JacksonXmlProperty(isAttribute = true, localName = "action") String action,
+ @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "attribute") List<Attribute> attributeList) {
+ this.name = name.toLowerCase();
+ this.attributeList = Optional.ofNullable(attributeList)
+ .map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.action = action.toLowerCase();
+
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ /**
+ * Indicates if the action for this tag matches the supplied action
+ *
+ * @param action The action to match against
+ * @return True if it matches
+ */
+ public boolean isAction(String action) {
+ return action.equals(this.action);
+ }
+
+ public Tag mutateAction(String action) {
+ return new Tag(this.name, action, this.attributeList);
+ }
+
+ public List<Attribute> getAttributeList() {
+ return attributeList;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map<String, Attribute> getAttributeMap() {
+ return attributeList.stream()
+ .collect(Collectors.toMap(Attribute::getName, Function.identity()));
+ }
+
+ public Attribute getAttributeByName(String name) {
+ Map<String, Attribute> attributeMap = getAttributeMap();
+ return attributeMap.get(name);
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/TagRules.java b/src/main/java/org/apache/sling/xss/impl/xml/TagRules.java
new file mode 100644
index 0000000..dec49e9
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/TagRules.java
@@ -0,0 +1,30 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class TagRules {
+ @JacksonXmlProperty(localName = "tag")
+ private List<Tag> tagRulesList = Collections.emptyList();
+
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/xml/TagsToEncode.java b/src/main/java/org/apache/sling/xss/impl/xml/TagsToEncode.java
new file mode 100644
index 0000000..8223a74
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/xml/TagsToEncode.java
@@ -0,0 +1,36 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.xss.impl.xml;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class TagsToEncode {
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "tag")
+ private List<String> tagToEncode = Collections.emptyList();
+
+ public List<String> getTagToEncode() {
+ return tagToEncode;
+ }
+
+}
diff --git a/src/main/java/org/owasp/html/DynamicAttributesSanitizerPolicy.java b/src/main/java/org/owasp/html/DynamicAttributesSanitizerPolicy.java
new file mode 100644
index 0000000..cd8b460
--- /dev/null
+++ b/src/main/java/org/owasp/html/DynamicAttributesSanitizerPolicy.java
@@ -0,0 +1,151 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.owasp.html;
+
+import java.lang.reflect.InvocationTargetException;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Extends the default policy to support dynamic attributes.
+ *
+ * Since we want to support the antisamy config file, we have to make dynamic
+ * tag attributes possible.
+ * It is placed in the 'org.owasp.html' package because there are package
+ * private fields.
+ */
+public class DynamicAttributesSanitizerPolicy extends ElementAndAttributePolicyBasedSanitizerPolicy {
+
+ private Map<String, ElementAndAttributePolicies> elementAndAttrPolicies;
+ private Map<String, AttributePolicy> dynamicAttributesPolicyMap;
+ private List<String> onInvalidRemoveTagList;
+ private int numberOfErrors;
+
+ public DynamicAttributesSanitizerPolicy(HtmlStreamEventReceiver out,
+ ImmutableMap<String, ElementAndAttributePolicies> elAndAttrPolicies,
+ ImmutableSet<String> allowedTextContainers,
+ Map<String, AttributePolicy> dynamicAttributesPolicyMap, List<String> onInvalidRemoveTagList) {
+ super(out, elAndAttrPolicies, allowedTextContainers);
+ this.elementAndAttrPolicies = elAndAttrPolicies;
+ this.dynamicAttributesPolicyMap = dynamicAttributesPolicyMap;
+ this.onInvalidRemoveTagList = onInvalidRemoveTagList;
+ }
+
+ @Override
+ public void openTag(String elementName, List<String> attrs) {
+ // StylingPolicy repeats some of this code because it is more complicated
+ // to refactor it into multiple method bodies, so if you change this,
+ // check the override of it in that class.
+ if (elementName != null && attrs != null && elementAndAttrPolicies != null) {
+ ElementAndAttributePolicies policies = elementAndAttrPolicies.get(elementName);
+
+ String adjustedElementName = applyPolicies2(elementName, attrs, policies);
+ if (adjustedElementName != null
+ && !(attrs.isEmpty() && policies.htmlTagSkipType.skipAvailability())) {
+ writeOpenTag(policies, adjustedElementName, attrs);
+ return;
+ }
+ deferOpenTag(elementName);
+ }
+ }
+
+ final @Nullable String applyPolicies2(
+ String elementName, List<String> attrs,
+ ElementAndAttributePolicies policies) {
+ String adjustedElementName;
+ Boolean removeTag = false;
+ if (policies != null) {
+ for (ListIterator<String> attrsIt = attrs.listIterator(); attrsIt.hasNext();) {
+ String name = attrsIt.next();
+
+ AttributePolicy attrPolicy = null;
+ // check if the attribute name starts with an dynamic tag, to handle it specially
+ for (Entry<String, AttributePolicy> dynamicAttributeEntry : dynamicAttributesPolicyMap.entrySet()) {
+ if (name.startsWith(dynamicAttributeEntry.getKey())) {
+ attrPolicy = dynamicAttributeEntry.getValue();
+ break;
+ }
+ }
+ // if it is not an dynamic attr it gets it's normal policy
+ if (attrPolicy == null) {
+ attrPolicy = policies.attrPolicies.get(name);
+ }
+
+ // if there is no policy for this attribute, it gets removed
+ if (attrPolicy == null) {
+ numberOfErrors++;
+ attrsIt.remove();
+ attrsIt.next();
+ attrsIt.remove();
+ } else {
+ String value = attrsIt.next();
+ String adjustedValue = attrPolicy.apply(elementName, name, value);
+ if (adjustedValue == null) {
+ numberOfErrors++;
+ if (onInvalidRemoveTagList.contains(name)) {
+ removeTag = true;
+ }
+ attrsIt.remove();
+ attrsIt.previous();
+ attrsIt.remove();
+ } else {
+ attrsIt.set(adjustedValue);
+ }
+ }
+ }
+
+ try {
+ Method removeDuplicateAttributesMethod = ElementAndAttributePolicyBasedSanitizerPolicy.class
+ .getDeclaredMethod("removeDuplicateAttributes", List.class);
+ removeDuplicateAttributesMethod.setAccessible(true);
+ try {
+ removeDuplicateAttributesMethod.invoke(null, attrs);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+
+ // checks if the onInvalid action of the invalid Tag is remove, and then removes
+ // it
+ adjustedElementName = Boolean.TRUE.equals(removeTag) ? null : policies.elPolicy.apply(elementName, attrs);
+ if (adjustedElementName != null) {
+ adjustedElementName = HtmlLexer.canonicalElementName(adjustedElementName);
+ }
+ } else {
+ numberOfErrors++;
+ adjustedElementName = null;
+ }
+ return adjustedElementName;
+ }
+
+ public int getNumberOfErrors() {
+ return numberOfErrors;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory b/src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory
deleted file mode 100644
index 0788ee2..0000000
--- a/src/main/resources/META-INF/services/javax.xml.transform.TransformerFactory
+++ /dev/null
@@ -1,18 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-org.apache.sling.xss.impl.AttributeTranslatingTransformerFactoryImpl
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/xss/impl/AntiSamyPolicyTest.java b/src/test/java/org/apache/sling/xss/impl/AntiSamyPolicyTest.java
index 65a4fd7..7b4d2d3 100644
--- a/src/test/java/org/apache/sling/xss/impl/AntiSamyPolicyTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/AntiSamyPolicyTest.java
@@ -21,15 +21,17 @@ package org.apache.sling.xss.impl;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import javax.xml.stream.XMLStreamException;
+
+import java.io.IOException;
import java.util.regex.Pattern;
+
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
-import org.owasp.validator.html.AntiSamy;
-import org.owasp.validator.html.Policy;
-import org.owasp.validator.html.PolicyException;
/**
@@ -40,32 +42,29 @@ import org.owasp.validator.html.PolicyException;
public class AntiSamyPolicyTest {
public static final String POLICY_FILE = "SLING-INF/content/config.xml";
- private static AntiSamy antiSamy;
+ private static HtmlSanitizer antiSamy;
@BeforeAll
- public static void setup() throws PolicyException {
- antiSamy = new AntiSamy(Policy.getInstance(AntiSamyPolicyTest.class.getClassLoader().getResourceAsStream(POLICY_FILE)));
+ public static void setup() throws InvalidConfigException, XMLStreamException, IOException {
+ antiSamy = new HtmlSanitizer(new AntiSamyPolicy(AntiSamyPolicyTest.class.getClassLoader().getResourceAsStream(POLICY_FILE)));
}
@ParameterizedTest
@MethodSource("dataForScriptFiltering")
public void testScriptFiltering(TestInput testInput) throws Exception {
- testInput.skipComparingInputWithOutput = false;
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
@ParameterizedTest
@MethodSource("dataForEventHandlerAttributes")
public void testEventHandlerAttributes(TestInput testInput) throws Exception {
- testInput.skipComparingInputWithOutput = false;
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
@ParameterizedTest
@MethodSource("dataForImageFiltering")
public void testImageFiltering(TestInput testInput) throws Exception {
- testInput.skipComparingInputWithOutput = false;
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
@ParameterizedTest
@@ -81,20 +80,20 @@ public class AntiSamyPolicyTest {
@ParameterizedTest
@MethodSource("dataForURIFiltering")
public void testURIFiltering(TestInput testInput) throws Exception {
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
@ParameterizedTest
@MethodSource("dataForCSSFiltering")
public void testCSSFiltering(TestInput testInput) throws Exception {
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
@ParameterizedTest
@MethodSource("dataForDataAttributes")
public void testDataAttributes(TestInput testInput) throws Exception {
testInput.skipComparingInputWithOutput = false;
- testInput.runCheck(Mode.SAX);
+ testInput.runCheck();
}
/**
@@ -103,14 +102,12 @@ public class AntiSamyPolicyTest {
@ParameterizedTest
@MethodSource("dataForIssueSLING8771")
public void testIssueSLING8771(TestInput testInput) throws Exception {
- testInput.runCheck(Mode.SAX_AND_DOM);
+ testInput.runCheck();
}
private void testOutputIsEmpty(String input) throws Exception {
- String cleanDOMModeHTML = antiSamy.scan(input, AntiSamy.DOM).getCleanHTML();
- String cleanSAXModeHTML = antiSamy.scan(input, AntiSamy.SAX).getCleanHTML();
- assertTrue(StringUtils.isEmpty(cleanDOMModeHTML), "Expected empty DOM filtered output for '" + input + "'.");
- assertTrue(StringUtils.isEmpty(cleanSAXModeHTML), "Expected empty SAX filtered output for '" + input + "'.");
+ String cleanHTML = antiSamy.scan(input).getSanitizedString();
+ assertTrue(StringUtils.isEmpty(cleanHTML), "Expected empty filtered output for '" + input + "'.");
}
static TestInput[] dataForScriptFiltering() {
@@ -269,67 +266,48 @@ public class AntiSamyPolicyTest {
this.skipComparingInputWithOutput = skipComparingInputWithOutput;
}
- void runCheck(Mode mode) throws Exception {
- String cleanDOMModeHTML = antiSamy.scan(input, AntiSamy.DOM).getCleanHTML();
- String cleanSAXModeHTML = antiSamy.scan(input, AntiSamy.SAX).getCleanHTML();
- if (!skipComparingInputWithOutput) {
- if(pattern != null){
- assertTrue(pattern.matcher(input.toLowerCase()).find(), String.format("Test is not properly configured: input '%s' doesn't seem to contain '%s' (case-insensitive match).",
- input,expectedPartialOutput));
- }
- assertTrue(input.toLowerCase().contains(expectedPartialOutput.toLowerCase()), String.format("Test is not properly configured: input '%s' doesn't seem to contain '%s' (case-insensitive match).",
- input,expectedPartialOutput));
- }
- if (containsExpectedPartialOutput) {
- if (mode == Mode.DOM || mode == Mode.SAX_AND_DOM) {
- if(pattern != null){
- assertTrue(
- pattern.matcher(antiSamy.scan(input, AntiSamy.DOM).getCleanHTML()).find(), String.format("Expected that DOM filtered output '%s' for input '%s' would contain '%s'.", cleanDOMModeHTML, input,
- expectedPartialOutput));
- } else {
- assertTrue(
- antiSamy.scan(input, AntiSamy.DOM).getCleanHTML().contains(expectedPartialOutput), String.format("Expected that DOM filtered output '%s' for input '%s' would contain '%s'.", cleanDOMModeHTML, input,
- expectedPartialOutput));
- }
- }
- if (mode == Mode.SAX || mode == Mode.SAX_AND_DOM) {
- if(pattern != null){
- assertTrue(pattern.matcher(antiSamy.scan(input, AntiSamy.SAX).getCleanHTML()).find(), String.format("Expected that SAX filtered output '%s' for input '%s' would contain '%s'.", cleanSAXModeHTML,
- input, expectedPartialOutput));
- } else {
- assertTrue(antiSamy.scan(input, AntiSamy.SAX).getCleanHTML().contains(expectedPartialOutput), String.format("Expected that SAX filtered output '%s' for input '%s' would contain '%s'.", cleanSAXModeHTML,
- input, expectedPartialOutput));
- }
- }
- } else {
- if (mode == Mode.DOM || mode == Mode.SAX_AND_DOM) {
- if(pattern != null){
- assertFalse(pattern.matcher(antiSamy.scan(input, AntiSamy.DOM).getCleanHTML()).find(),
- String.format("Expected that DOM filtered output '%s' for input '%s', would NOT contain '%s'.", cleanDOMModeHTML,
- input, expectedPartialOutput));
- } else {
- assertFalse(antiSamy.scan(input, AntiSamy.DOM).getCleanHTML().contains(expectedPartialOutput),
- String.format("Expected that DOM filtered output '%s' for input '%s', would NOT contain '%s'.", cleanDOMModeHTML,
- input, expectedPartialOutput));
- }
- }
- if (mode == Mode.SAX || mode == Mode.SAX_AND_DOM) {
- if(pattern != null){
- assertFalse(pattern.matcher(antiSamy.scan(input, AntiSamy.SAX).getCleanHTML()).find(), String.format("Expected that SAX filtered output '%s' for input '%s' would NOT contain '%s'.", cleanSAXModeHTML,
- input, expectedPartialOutput));
- }
- else {
- assertFalse(antiSamy.scan(input, AntiSamy.SAX).getCleanHTML().contains(expectedPartialOutput), String.format("Expected that SAX filtered output '%s' for input '%s' would NOT contain '%s'.", cleanSAXModeHTML,
- input, expectedPartialOutput));
- }
- }
- }
- }
-
+ void runCheck() throws Exception {
+ String cleanHTML = antiSamy.scan(input).getSanitizedString();
+ if (!skipComparingInputWithOutput) {
+ if (pattern != null) {
+ assertTrue(pattern.matcher(input.toLowerCase()).find(), String.format(
+ "Test is not properly configured: input '%s' doesn't seem to partialy matcht to following pattern:'%s' (case-insensitive match).",
+ input, expectedPartialOutput.toString()));
+ } else {
+ assertTrue(input.toLowerCase().contains(expectedPartialOutput.toLowerCase()), String.format(
+ "Test is not properly configured: input '%s' doesn't seem to contain '%s' (case-insensitive match).",
+ input, expectedPartialOutput));
+ }
+ }
+ if (containsExpectedPartialOutput) {
+ if (pattern != null) {
+ assertTrue(
+ pattern.matcher(antiSamy.scan(input).getSanitizedString()).find(),
+ String.format("Expected that filtered output '%s' for input '%s' would partialy match to following pattern: '%s'.",
+ cleanHTML,
+ input,
+ expectedPartialOutput));
+ } else {
+ assertTrue(
+ antiSamy.scan(input).getSanitizedString().contains(expectedPartialOutput),
+ String.format("Expected that filtered output '%s' for input '%s' would contain '%s'.",
+ cleanHTML,
+ input,
+ expectedPartialOutput));
+ }
+ } else {
+ if (pattern != null) {
+ assertFalse(pattern.matcher(antiSamy.scan(input).getSanitizedString()).find(),
+ String.format("Expected that filtered output '%s' for input '%s', would NOT partialy match to following pattern:: '%s'.",
+ cleanHTML,
+ input, expectedPartialOutput));
+ } else {
+ assertFalse(antiSamy.scan(input).getSanitizedString().contains(expectedPartialOutput),
+ String.format("Expected that filtered output '%s' for input '%s', would NOT contain '%s'.",
+ cleanHTML,
+ input, expectedPartialOutput));
+ }
+ }
+ }
}
-
- private enum Mode {
- SAX, DOM, SAX_AND_DOM;
- }
-}
-
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
index f93e41c..b3a2576 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
@@ -34,6 +34,7 @@ import org.apache.sling.testing.mock.sling.junit5.SlingContext;
import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
import org.apache.sling.xss.XSSAPI;
import org.apache.sling.xss.impl.status.XSSStatusService;
+import org.apache.sling.xss.impl.xml.Attribute;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -41,7 +42,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.osgi.framework.ServiceReference;
-import org.owasp.validator.html.model.Attribute;
import org.powermock.reflect.Whitebox;
@ExtendWith(SlingContextExtension.class)
@@ -335,6 +335,15 @@ public class XSSAPIImplTest {
static String[][] dataForFilterHtml() {
return new String[][] {
// Source Expected Result
+
+ // checking if both, the literal and the regex check are executed
+ { "<link media=\"screen\">hello</link>", "<link media=\"screen\" />hello" },
+ { "<link media=\"checkRege10\">hello</link>", "<link media=\"checkRege10\" />hello" },
+
+ { "<div align=\"center\">valid Test</div>", "<div align=\"center\">valid Test</div>" },
+ { "<style media=\"screen\">h1 {color:red;}</style>", "<style media=\"screen\">h1 {\n\tcolor: red;\n}\n</style>" },
+ { "<object id=\"center\" type=\"application/x-shockwave-flash\">valid Test</object>", "<object id=\"center\" type=\"application/x-shockwave-flash\">valid Test</object>" },
+
{null, ""},
{"", ""},
{"simple", "simple"},
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
index ce44f8e..444501d 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
@@ -61,6 +61,21 @@ public class XSSFilterImplTest {
return testData;
}
+ static List<Object[]> dataForCheckMethod() {
+ List<Object[]> testData = new ArrayList<>();
+ testData.add(new Object[] { "<link media=\"screen\">hello</link>", true });
+ testData.add(new Object[] { "<link media=\"testingRege10\">hello</link>", true });
+ testData.add(new Object[] { "<style media=\"screen\">h1 {color:red;}</style>", true });
+ testData.add(new Object[] { "<link type=\"text/css\">valid Test</link>", true });
+ testData.add(new Object[] { "<body bgcolor=\"black\">valid Test</body>", true });
+ testData.add(new Object[] { "<div background=\"green\">invalid Test</div>", false });
+ testData.add(new Object[] { "<table border=\"3\">valid Test</table>", true });
+ testData.add(new Object[] { "<table border=\"green\">invalid Test</table>", false });
+ testData.add(new Object[] { "<script>invalid Test</script>", false });
+ testData.add(new Object[] { "", false });
+ return testData;
+ }
+
public SlingContext context = new SlingContext();
private XSSFilter xssFilter;
@@ -101,6 +116,19 @@ public class XSSFilterImplTest {
assertEquals(XSSFilterImpl.EMBEDDED_POLICY_PATH, antiSamyPolicy.getPath(), "This is not the policy we're looking for.");
}
+ @ParameterizedTest
+ @MethodSource("dataForCheckMethod")
+ public void testCheckMethod(String input, boolean isValid) {
+ context.registerInjectActivateService(new XSSFilterImpl());
+ xssFilter = context.getService(XSSFilter.class);
+ System.out.println(input);
+ if (isValid) {
+ assertTrue(xssFilter.check(XSSFilter.DEFAULT_CONTEXT, input), "Expected valid input value for: " + input);
+ } else {
+ assertFalse(xssFilter.check(XSSFilter.DEFAULT_CONTEXT, input), "Expected invalid input value for: " + input);
+ }
+ }
+
@ParameterizedTest
@MethodSource("dataForValidHref")
public void isValidHref(String input, boolean isValid) {
diff --git a/src/test/java/org/apache/sling/xss/impl/xml/PolicyTest.java b/src/test/java/org/apache/sling/xss/impl/xml/PolicyTest.java
new file mode 100644
index 0000000..a512fa1
--- /dev/null
+++ b/src/test/java/org/apache/sling/xss/impl/xml/PolicyTest.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one or
+ * more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to you under the
+ * Apache License, Version 2.0 (the "License"); you may not use
+ * this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0 Unless required by
+ * applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ ******************************************************************************/
+package org.apache.sling.xss.impl.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.sling.xss.impl.InvalidConfigException;
+import org.apache.sling.xss.impl.xml.AntiSamyPolicy.CssPolicy;
+import org.junit.jupiter.api.Test;
+
+class PolicyTest {
+
+ @Test
+ void loadDefaultPolicy() throws Exception, InvalidConfigException {
+ try(InputStream input = AntiSamyPolicy.class.getClassLoader().getResourceAsStream("SLING-INF/content/config.xml")) {
+ AntiSamyPolicy policy = new AntiSamyPolicy(input);
+ Map<String, Pattern> regexp = policy.getCommonRegularExpressions();
+ List<String> empty = policy.getAllowedEmptyTags();
+ List<String> closingTag = policy.getRequireClosingTags();
+ Map<String, Attribute> global = policy.getGlobalAttributes();
+ Map<String, Attribute> dynamic = policy.getDynamicAttributes();
+ Map<String, Attribute> commonAttr = policy.getCommonAttributes();
+ Map<String, Tag> tagRules = policy.getTagRules();
+ Map<String, Property> cssRules = policy.getCssRules();
+ Map<String, String> directives = policy.getDirectives();
+
+ assertNotNull(policy);
+ Tag imgTag = policy.getTagRules().get("img");
+ assertNotNull(imgTag, "img tag rules");
+ assertEquals(9, imgTag.getAttributeList().size(), "number of known img tag attributes");
+ assertEquals(41, regexp.size(), "number of known common regexs");
+ assertEquals(19, empty.size(), "number of known allowed emty tags");
+ assertEquals(5, global.size(), "number of known global attributes");
+ assertEquals(1, dynamic.size(), "number of known dynamic attributes");
+ assertEquals(0, closingTag.size(), "number of known closing Tags");
+ assertEquals(46, commonAttr.size(), "number of known common attributes");
+ assertEquals(73, tagRules.size(), "number of known tag rules");
+ assertEquals(118, cssRules.size(), "number of known css rules");
+ assertEquals(12, directives.size(), "number of known directives");
+
+ CssPolicy cssPolicy = policy.getCssPolicy();
+
+ assertEquals(118, cssPolicy.getCssRules().size(), "cssPolicy.cssRules.size");
+ assertTrue(cssPolicy.isValidElementName("base-link"));
+ assertFalse(cssPolicy.isValidElementName("base|link"));
+ }
+ }
+}