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 <[email protected]> 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);
