You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2020/01/07 17:38:13 UTC
[sling-org-apache-sling-xss] 01/01: SLING-8866 - Add reporting info
in the XSS Protection API bundle
This is an automated email from the ASF dual-hosted git repository.
radu pushed a commit to branch issue/SLING-8866
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git
commit d8a4829e78b0f8f26e115e52f8e01a6f7c5d73e1
Author: Radu Cotescu <ra...@apache.org>
AuthorDate: Tue Jan 7 18:37:45 2020 +0100
SLING-8866 - Add reporting info in the XSS Protection API bundle
* added a counter metric (sling:xss.invalid_hrefs) to track the number of invalid
URLs detected by org.apache.sling.xss.XSSFilter#isValidHref
* enhanced the webconsole plugin to provide a detailed report of the blocked
URLs
* allow a system administrator to download the active AntiSamy configuration
---
pom.xml | 12 ++
.../java/org/apache/sling/xss/impl/XSSAPIImpl.java | 5 -
.../org/apache/sling/xss/impl/XSSFilterImpl.java | 27 ++-
.../xss/impl/XSSProtectionAPIWebConsolePlugin.java | 117 -------------
.../apache/sling/xss/impl/status/FixedSizeMap.java | 57 +++++++
.../sling/xss/impl/status/XSSStatusService.java | 107 ++++++++++++
.../XSSProtectionAPIWebConsolePlugin.java | 185 +++++++++++++++++++++
src/main/resources/webconsole/blocked.js | 21 +++
src/main/resources/webconsole/config.js | 24 +++
.../resources/{res/ui => webconsole}/prettify.css | 6 +
.../resources/{res/ui => webconsole}/prettify.js | 0
src/main/resources/webconsole/xss.css | 24 +++
src/main/resources/webconsole/xss.js | 21 +++
.../org/apache/sling/xss/impl/XSSAPIImplTest.java | 19 ++-
.../apache/sling/xss/impl/XSSFilterImplTest.java | 18 +-
15 files changed, 510 insertions(+), 133 deletions(-)
diff --git a/pom.xml b/pom.xml
index 370aac4..db431fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -258,6 +258,18 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.metrics</artifactId>
+ <version>1.2.6</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.dropwizard.metrics</groupId>
+ <artifactId>metrics-core</artifactId>
+ <version>3.2.3</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
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 958e365..68ed5c9 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
@@ -18,13 +18,8 @@ package org.apache.sling.xss.impl;
import java.io.StringReader;
import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.json.Json;
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 64cc434..ad9e5cd 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSFilterImpl.java
@@ -35,9 +35,12 @@ import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
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.jetbrains.annotations.NotNull;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
@@ -164,6 +167,15 @@ public class XSSFilterImpl implements XSSFilter {
@Reference
private ServiceUserMapped serviceUserMapped;
+ @Reference
+ private MetricsService metricsService;
+
+ @Reference
+ private XSSStatusService statusService;
+
+ private Counter invalidHrefs;
+ private static final String COUNTER_INVALID_HREFS = "xss.invalid_hrefs";
+
@Override
public boolean check(final ProtectionContext context, final String src) {
final XSSFilterRule ctx = this.getFilterRule(context);
@@ -204,7 +216,7 @@ public class XSSFilterImpl implements XSSFilter {
return false;
}
- AntiSamyPolicy getActivePolicy() {
+ public AntiSamyPolicy getActivePolicy() {
return activePolicy;
}
@@ -228,12 +240,17 @@ public class XSSFilterImpl implements XSSFilter {
}
}
}
+ if (!isValid) {
+ statusService.reportInvalidUrl(url);
+ invalidHrefs.increment();
+ }
return isValid;
}
@Activate
@Modified
protected void activate(ComponentContext componentContext, Configuration configuration) {
+ invalidHrefs = metricsService.counter(COUNTER_INVALID_HREFS);
// load default handler
policyPath = configuration.policyPath();
updatePolicy();
@@ -340,7 +357,7 @@ public class XSSFilterImpl implements XSSFilter {
}
}
- class AntiSamyPolicy {
+ public class AntiSamyPolicy {
private final boolean embedded;
private final String path;
@@ -349,7 +366,7 @@ public class XSSFilterImpl implements XSSFilter {
this.path = path;
}
- InputStream read() {
+ public InputStream read() {
if (embedded) {
return this.getClass().getClassLoader().getResourceAsStream(EMBEDDED_POLICY_PATH);
}
@@ -365,11 +382,11 @@ public class XSSFilterImpl implements XSSFilter {
}
}
- boolean isEmbedded() {
+ public boolean isEmbedded() {
return embedded;
}
- String getPath() {
+ public String getPath() {
return path;
}
}
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java b/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java
deleted file mode 100644
index 8e77e28..0000000
--- a/src/main/java/org/apache/sling/xss/impl/XSSProtectionAPIWebConsolePlugin.java
+++ /dev/null
@@ -1,117 +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 java.io.IOException;
-import java.io.InputStream;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-
-import javax.servlet.Servlet;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.apache.sling.xss.XSSFilter;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.Reference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Component(
- service = Servlet.class,
- property = {
- XSSProtectionAPIWebConsolePlugin.REG_PROP_LABEL + "=" + XSSProtectionAPIWebConsolePlugin.LABEL,
- XSSProtectionAPIWebConsolePlugin.REG_PROP_TITLE + "=" + XSSProtectionAPIWebConsolePlugin.TITLE,
- XSSProtectionAPIWebConsolePlugin.REG_PROP_CATEGORY + "=Sling"
- }
-)
-public class XSSProtectionAPIWebConsolePlugin extends HttpServlet {
-
- /*
- do not replace the following constants with the ones from org.apache.felix, since you'll create a wiring to those APIs; the
- current way this plugin is written allows it to optionally be available, if the Felix Web Console is installed on the OSGi
- platform where this bundle will be deployed
- */
- static final String REG_PROP_LABEL = "felix.webconsole.label";
- static final String REG_PROP_TITLE = "felix.webconsole.title";
- static final String REG_PROP_CATEGORY = "felix.webconsole.category";
- static final String LABEL = "xssprotection";
- static final String TITLE= "XSS Protection";
-
- private static final String RES_LOC = LABEL + "/res/ui";
- private static final Logger LOGGER = LoggerFactory.getLogger(XSSProtectionAPIWebConsolePlugin.class);
-
- @Reference(target = "(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
- private XSSFilter xssFilter;
-
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
- if (request.getRequestURI().endsWith(RES_LOC + "/prettify.css")) {
- try(InputStream cssStream = getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.css")) {
- if (cssStream != null) {
- response.setContentType("text/css");
- IOUtils.copy(cssStream, response.getOutputStream());
- }
- }
- } else if (request.getRequestURI().endsWith(RES_LOC + "/prettify.js")) {
- try(InputStream jsStream = getClass().getClassLoader().getResourceAsStream("/res/ui/prettify.js")) {
- if (jsStream != null) {
- response.setContentType("application/javascript");
- IOUtils.copy(jsStream, response.getOutputStream());
- }
- }
- } else {
- if (xssFilter != null) {
- XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
- XSSFilterImpl.AntiSamyPolicy antiSamyPolicy = xssFilterImpl.getActivePolicy();
- if (antiSamyPolicy != null) {
- Writer w = response.getWriter();
- w.write("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + RES_LOC + "/prettify.css\"></link>");
- w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/prettify.js\"></script>");
- w.write("<script type=\"text/javascript\" src=\"" + RES_LOC + "/fsclassloader.js\"></script>");
- w.write("<script>$(document).ready(prettyPrint);</script>");
- w.write("<style>.prettyprint ol.linenums > li { list-style-type: decimal; } pre.prettyprint { white-space: pre-wrap; " +
- "}</style>");
- w.write("<p class=\"statline ui-state-highlight\">The current AntiSamy configuration ");
- if (antiSamyPolicy.isEmbedded()) {
- w.write("is the default one embedded in the org.apache.sling.xss bundle.");
- } else {
- w.write("is loaded from ");
- w.write(antiSamyPolicy.getPath());
- w.write(".");
- }
- w.write("</p>");
- String contents = "";
- try(InputStream configurationStream = antiSamyPolicy.read()) {
- contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8);
- } catch (Throwable t) {
- LOGGER.error("Unable to read policy file.", t);
- }
- w.write("<pre class=\"prettyprint linenums\">");
- w.write(StringEscapeUtils.escapeHtml4(contents));
- w.write("</pre>");
- }
- }
-
- }
- }
-}
diff --git a/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java b/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java
new file mode 100644
index 0000000..6504a22
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/status/FixedSizeMap.java
@@ -0,0 +1,57 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.status;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class FixedSizeMap<K, V> extends LinkedHashMap<K, V> {
+
+ private final int maxSize;
+
+ public FixedSizeMap(int initialCapacity, float loadFactor, int maxSize) {
+ super(initialCapacity, loadFactor);
+ this.maxSize = maxSize;
+ }
+
+ public FixedSizeMap(int initialCapacity, int maxSize) {
+ super(initialCapacity);
+ this.maxSize = maxSize;
+ }
+
+ public FixedSizeMap(int maxSize) {
+ super();
+ this.maxSize = maxSize;
+ }
+
+ public FixedSizeMap(Map<? extends K, ? extends V> m, int maxSize) {
+ super(m);
+ this.maxSize = maxSize;
+ }
+
+ public FixedSizeMap(int initialCapacity, float loadFactor, boolean accessOrder, int maxSize) {
+ super(initialCapacity, loadFactor, accessOrder);
+ this.maxSize = maxSize;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > maxSize;
+ }
+}
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
new file mode 100644
index 0000000..d0e28f9
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/status/XSSStatusService.java
@@ -0,0 +1,107 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * The {@code XSSLibraryStatusService} collects information about the way the XSS Protection API library is used.
+ */
+@Component(service = XSSStatusService.class)
+@Designate(ocd = XSSStatusService.Configuration.class)
+public class XSSStatusService {
+
+ @ObjectClassDefinition(
+ name = "Apache Sling XSS Status Service",
+ description = "The XSS Protection API Status Service provides various statistics about how the library was used."
+ )
+ @interface Configuration {
+ @AttributeDefinition(
+ name = "Maximum number of recorded invalid URLs",
+ description = "Once this number is reached, previously recorded invalid URLs will be discarded."
+ )
+ int maxNumberOfInvalidUrlsRecorded() default MAX_INVALID_URLS_RECORDED;
+ }
+
+ public static final int MAX_INVALID_URLS_RECORDED = 1000;
+
+ private Map<String, AtomicInteger> invalidUrls;
+
+ public void reportInvalidUrl(@NotNull String url) {
+ if (invalidUrls.containsKey(url)) {
+ invalidUrls.get(url).incrementAndGet();
+ } else {
+ invalidUrls.put(url, new AtomicInteger(1));
+ }
+ }
+
+ public Map<String, AtomicInteger> getInvalidUrls() {
+ synchronized (invalidUrls) {
+ return sortByNumericValue(invalidUrls);
+ }
+ }
+
+ @Activate
+ private void activate(Configuration configuration) {
+ invalidUrls = Collections.synchronizedMap(new FixedSizeMap<>(configuration.maxNumberOfInvalidUrlsRecorded()));
+ }
+
+ private static <K, V extends Comparable<? super V>> Map<K, V> sortByComparableValue(Map<K, V> map) {
+ List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
+ list.sort(Map.Entry.comparingByValue());
+
+ Map<K, V> result = new LinkedHashMap<>();
+ for (Map.Entry<K, V> entry : list) {
+ result.put(entry.getKey(), entry.getValue());
+ }
+ return result;
+ }
+
+ private static <K, V extends Number> Map<K, V> sortByNumericValue(Map<K, V> map) {
+ List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet());
+ list.sort((left, right) -> {
+ double leftNumber = left.getValue().doubleValue();
+ double rightNumber = right.getValue().doubleValue();
+ if (leftNumber < rightNumber) {
+ return -1;
+ } else if (leftNumber > rightNumber) {
+ return 1;
+ }
+ return 0;
+ });
+
+ Map<K, V> result = new LinkedHashMap<>();
+ for (Map.Entry<K, V> entry : list) {
+ result.put(entry.getKey(), entry.getValue());
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
new file mode 100644
index 0000000..c915993
--- /dev/null
+++ b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
@@ -0,0 +1,185 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.webconsole;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.sling.xss.XSSFilter;
+import org.apache.sling.xss.impl.XSSFilterImpl;
+import org.apache.sling.xss.impl.status.XSSStatusService;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(
+ service = Servlet.class,
+ property = {
+ XSSProtectionAPIWebConsolePlugin.REG_PROP_LABEL + "=" + XSSProtectionAPIWebConsolePlugin.LABEL,
+ XSSProtectionAPIWebConsolePlugin.REG_PROP_TITLE + "=" + XSSProtectionAPIWebConsolePlugin.TITLE,
+ XSSProtectionAPIWebConsolePlugin.REG_PROP_CATEGORY + "=Sling"
+ }
+)
+public class XSSProtectionAPIWebConsolePlugin extends HttpServlet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(XSSProtectionAPIWebConsolePlugin.class);
+ /*
+ do not replace the following constants with the ones from org.apache.felix, since you'll create a wiring to those APIs; the
+ current way this plugin is written allows it to optionally be available, if the Felix Web Console is installed on the OSGi
+ platform where this bundle will be deployed
+ */
+ static final String REG_PROP_LABEL = "felix.webconsole.label";
+ static final String REG_PROP_TITLE = "felix.webconsole.title";
+ static final String REG_PROP_CATEGORY = "felix.webconsole.category";
+ static final String LABEL = "xssprotection";
+ static final String TITLE= "XSS Protection";
+
+ private static final String URI_ROOT = "/system/console/" + LABEL;
+ private static final String URI_CONFIG_XHR = URI_ROOT + "/config.xhr";
+ private static final String URI_BLOCKED_XHR = URI_ROOT + "/blocked.xhr";
+ private static final String URI_CONFIG_XML = URI_ROOT + "/config.xml";
+ private static final String INTERNAL_RESOURCES_FOLDER = "/webconsole";
+ private static final String RES_ROOT = URI_ROOT + INTERNAL_RESOURCES_FOLDER;
+ private static final String RES_URI_PRETTIFY_CSS = RES_ROOT + "/prettify.css";
+ private static final String RES_URI_PRETTIFY_JS = RES_ROOT + "/prettify.js";
+ private static final String RES_URI_XSS_CSS = RES_ROOT + "/xss.css";
+ private static final String RES_URI_XSS_JS = RES_ROOT + "/xss.js";
+ private static final String RES_URI_BLOCKED_JS = RES_ROOT + "/blocked.js";
+ private static final String RES_URI_CONFIG_JS = RES_ROOT + "/config.js";
+
+ @Reference(target = "(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
+ private XSSFilter xssFilter;
+
+ @Reference
+ private XSSStatusService statusService;
+
+ private static final Set<String> CSS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_CSS, RES_URI_XSS_CSS));
+ private static final Set<String> JS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_JS, RES_URI_XSS_JS, RES_URI_BLOCKED_JS,
+ RES_URI_CONFIG_JS));
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ String file = FilenameUtils.getName(request.getRequestURI());
+ if (file != null && CSS_RESOURCES.contains(request.getRequestURI())) {
+ try(InputStream cssStream =
+ getClass().getClassLoader().getResourceAsStream(INTERNAL_RESOURCES_FOLDER + "/" + file)) {
+ if (cssStream != null) {
+ response.setContentType("text/css");
+ IOUtils.copy(cssStream, response.getOutputStream());
+ }
+ }
+ } else if (file != null && JS_RESOURCES.contains(request.getRequestURI())) {
+ try (InputStream jsStream =
+ getClass().getClassLoader().getResourceAsStream(INTERNAL_RESOURCES_FOLDER + "/" + file)) {
+ if (jsStream != null) {
+ response.setContentType("application/javascript");
+ IOUtils.copy(jsStream, response.getOutputStream());
+ }
+ }
+ } else if (URI_CONFIG_XHR.equalsIgnoreCase(request.getRequestURI()) && xssFilter != null) {
+ response.setContentType("text/html");
+ XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
+ XSSFilterImpl.AntiSamyPolicy antiSamyPolicy = xssFilterImpl.getActivePolicy();
+ if (antiSamyPolicy != null) {
+ PrintWriter printWriter = response.getWriter();
+ printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_CONFIG_JS);
+ printWriter.write("<div id='config'>");
+ printWriter.printf("<link rel='stylesheet' type='text/css' href='%s'></link>\n", RES_URI_PRETTIFY_CSS);
+ printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_PRETTIFY_JS);
+ printWriter.write("<p class='statline ui-state-highlight'>The current AntiSamy configuration ");
+ if (antiSamyPolicy.isEmbedded()) {
+ printWriter.write("is the default one embedded in the org.apache.sling.xss bundle.");
+ } else {
+ printWriter.printf("is loaded from %s.", antiSamyPolicy.getPath());
+ }
+ printWriter.write("<button style='float:right' type='button' id='download-config'>Download</button></p>");
+ String contents = "";
+ try (InputStream configurationStream = antiSamyPolicy.read()) {
+ contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8);
+ } catch (Throwable t) {
+ LOGGER.error("Unable to read policy file.", t);
+ }
+ printWriter.write("<pre class='prettyprint linenums'>");
+ printWriter.write(StringEscapeUtils.escapeHtml4(contents));
+ printWriter.write("</pre>");
+ printWriter.write("</div>");
+ }
+ } else if (URI_BLOCKED_XHR.equalsIgnoreCase(request.getRequestURI())) {
+ response.setContentType("text/html");
+ PrintWriter printWriter = response.getWriter();
+ printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_BLOCKED_JS);
+ printWriter.println("<div id='blocked'>");
+ printWriter.println("<div class='table'>");
+ printWriter.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Blocked URLs</div>");
+ printWriter.println("<table class='nicetable tablesorter' id='invalid-urls'>");
+ printWriter.println("<thead>");
+ printWriter.println("<tr>");
+ printWriter.println("<th class='header'>URL</th>");
+ printWriter.println("<th class='header'>Times Blocked</th>");
+ printWriter.println("</tr>");
+ printWriter.println("</thead>");
+ printWriter.println("<tbody>");
+ int i = 1;
+ for (Map.Entry<String, AtomicInteger> entry : statusService.getInvalidUrls().entrySet()) {
+ String cssClass = ((i++ %2) == 0 ? "even" : "odd");
+ printWriter.printf("<tr class='%s ui-state-default'>%n<td>%s</td><td>%d</td></tr>", cssClass, entry.getKey(),
+ entry.getValue().intValue());
+ }
+ printWriter.println("</tbody>");
+ printWriter.println("</table>");
+ printWriter.println("</div>");
+ printWriter.println("</div>");
+ printWriter.println("</div>");
+ } else if (URI_CONFIG_XML.equalsIgnoreCase(request.getRequestURI()) && xssFilter != null) {
+ response.setContentType("application/xml");
+ response.setHeader("Content-Disposition", "attachment; filename=config.xml");
+ XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
+ IOUtils.copy(xssFilterImpl.getActivePolicy().read(), response.getOutputStream());
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ PrintWriter printWriter = response.getWriter();
+ printWriter.printf("<link rel='stylesheet' type='text/css' href='%s'>\n", RES_URI_XSS_CSS);
+ printWriter.printf("<script type='text/javascript' src='%s'></script>\n", RES_URI_XSS_JS);
+ printWriter.println("<div id='xss-tabs'>");
+ printWriter.println("<ul>");
+ printWriter.printf("<li><a href='%s'><span>Blocked URLs</span></a></li>\n", URI_BLOCKED_XHR);
+ if (xssFilter != null) {
+ printWriter.printf("<li><a href='%s'><span>Active Configuration</span></a></li>\n", URI_CONFIG_XHR);
+ }
+ printWriter.println("</ul>");
+ printWriter.println("</div>");
+ }
+ }
+}
diff --git a/src/main/resources/webconsole/blocked.js b/src/main/resources/webconsole/blocked.js
new file mode 100644
index 0000000..cc26b4c
--- /dev/null
+++ b/src/main/resources/webconsole/blocked.js
@@ -0,0 +1,21 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+ $('#invalid-urls').tablesorter();
+});
diff --git a/src/main/resources/webconsole/config.js b/src/main/resources/webconsole/config.js
new file mode 100644
index 0000000..4080fa6
--- /dev/null
+++ b/src/main/resources/webconsole/config.js
@@ -0,0 +1,24 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+ prettyPrint();
+ $('#download-config').on('click', function () {
+ window.location = window.location + '/config.xml';
+ });
+});
diff --git a/src/main/resources/res/ui/prettify.css b/src/main/resources/webconsole/prettify.css
similarity index 91%
rename from src/main/resources/res/ui/prettify.css
rename to src/main/resources/webconsole/prettify.css
index 0920749..ebcc414 100644
--- a/src/main/resources/res/ui/prettify.css
+++ b/src/main/resources/webconsole/prettify.css
@@ -15,3 +15,9 @@
* limitations under the License.
*/
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.clo,.opn,.pun{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.kwd,.tag,.typ{font-weight:700}.str{color:#060}.kwd{color:#006}.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.clo,.opn,.pun{color:#440}.tag{color:#006}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px soli [...]
+.prettyprint ol.linenums > li {
+ list-style-type: decimal;
+}
+pre.prettyprint {
+ white-space: pre-wrap;
+}
diff --git a/src/main/resources/res/ui/prettify.js b/src/main/resources/webconsole/prettify.js
similarity index 100%
rename from src/main/resources/res/ui/prettify.js
rename to src/main/resources/webconsole/prettify.js
diff --git a/src/main/resources/webconsole/xss.css b/src/main/resources/webconsole/xss.css
new file mode 100644
index 0000000..91c83fc
--- /dev/null
+++ b/src/main/resources/webconsole/xss.css
@@ -0,0 +1,24 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+.ui-tabs .ui-tabs-panel {
+ display: block;
+ border-width: 0;
+ padding: 1em 4em;
+ background: none;
+}
diff --git a/src/main/resources/webconsole/xss.js b/src/main/resources/webconsole/xss.js
new file mode 100644
index 0000000..8da50e4
--- /dev/null
+++ b/src/main/resources/webconsole/xss.js
@@ -0,0 +1,21 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+$(document).ready(function() {
+ $('#xss-tabs').tabs();
+});
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 43981f5..e480b66 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
@@ -20,10 +20,14 @@ import java.util.HashMap;
import java.util.regex.Pattern;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.apache.sling.testing.mock.sling.junit.SlingContext;
import org.apache.sling.xss.XSSAPI;
+import org.apache.sling.xss.impl.status.XSSStatusService;
import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.osgi.framework.ServiceReference;
@@ -35,7 +39,9 @@ import junit.framework.TestCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
public class XSSAPIImplTest {
@@ -55,12 +61,20 @@ public class XSSAPIImplTest {
* The only exception currently is {@link #testGetValidHrefWithoutHrefConfig()}.
*/
private void setUp() {
- context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
context.registerInjectActivateService(new XSSFilterImpl());
context.registerInjectActivateService(new XSSAPIImpl());
xssAPI = context.getService(XSSAPI.class);
}
+ @Before
+ public void before() {
+ MetricsService metricsService = mock(MetricsService.class);
+ when(metricsService.counter(anyString())).thenReturn(mock(Counter.class));
+ context.registerService(MetricsService.class, metricsService);
+ context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+ context.registerService(new XSSStatusService());
+ }
+
@After
public void tearDown() {
xssAPI = null;
@@ -350,8 +364,7 @@ public class XSSAPIImplTest {
}
@Test
- public void testGetValidHrefWithoutHrefConfig() throws Exception {
- context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+ public void testGetValidHrefWithoutHrefConfig() {
context.load().binaryFile("/configWithoutHref.xml", "/apps/sling/xss/configWithoutHref.xml");
context.registerInjectActivateService(new XSSFilterImpl(), new HashMap<String, Object>(){{
put("policyPath", "/apps/sling/xss/configWithoutHref.xml");
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 f1f26e9..4c6ead3 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSFilterImplTest.java
@@ -18,17 +18,23 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.xss.impl;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.apache.sling.testing.mock.sling.junit.SlingContext;
import org.apache.sling.xss.XSSFilter;
+import org.apache.sling.xss.impl.status.XSSStatusService;
import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
public class XSSFilterImplTest {
@@ -42,11 +48,19 @@ public class XSSFilterImplTest {
xssFilter = null;
}
+ @Before
+ public void setUp() {
+ MetricsService metricsService = mock(MetricsService.class);
+ when(metricsService.counter(anyString())).thenReturn(mock(Counter.class));
+ context.registerService(MetricsService.class, metricsService);
+ context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
+ context.registerService(new XSSStatusService());
+ }
+
@Test
public void testResourceBasedPolicy() {
context.load().binaryFile(this.getClass().getClassLoader().getResourceAsStream(XSSFilterImpl.EMBEDDED_POLICY_PATH),
"/libs/" + XSSFilterImpl.DEFAULT_POLICY_PATH);
- context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
context.registerInjectActivateService(new XSSFilterImpl());
xssFilter = context.getService(XSSFilter.class);
XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
@@ -57,7 +71,6 @@ public class XSSFilterImplTest {
@Test
public void testDefaultEmbeddedPolicy() {
- context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
context.registerInjectActivateService(new XSSFilterImpl());
xssFilter = context.getService(XSSFilter.class);
XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
@@ -68,7 +81,6 @@ public class XSSFilterImplTest {
@Test
public void isValidHref() {
- context.registerService(ServiceUserMapped.class, mock(ServiceUserMapped.class));
context.registerInjectActivateService(new XSSFilterImpl());
xssFilter = context.getService(XSSFilter.class);
checkIsValid("javascript:alert(1)", false);