This is an automated email from the ASF dual-hosted git repository.
joerghoh 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 75d268d SLING-13335 Insufficient output encoding in Webconsole plugin
75d268d is described below
commit 75d268dba46a5365313ce26b870d78907fc9b969
Author: Joerg Hoh <[email protected]>
AuthorDate: Mon Sep 14 17:57:22 2026 +0200
SLING-13335 Insufficient output encoding in Webconsole plugin
---
.../XSSProtectionAPIWebConsolePlugin.java | 35 ++++++++--
src/main/resources/webconsole/xss.js | 15 ++--
.../XSSProtectionAPIWebConsolePluginTest.java | 79 ++++++++++++++++++++++
3 files changed, 118 insertions(+), 11 deletions(-)
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
index 1150bdf..835eb0a 100644
---
a/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
+++
b/src/main/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePlugin.java
@@ -82,8 +82,18 @@ public class XSSProtectionAPIWebConsolePlugin extends
HttpServlet {
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";
- public static final String SCRIPT_TAG = "<script src='%s'></script>\n";
- public static final String LINK_TAG = "<link rel='stylesheet'
type='text/css' href='%s'>";
+ /*
+ request-derived values are interpolated into these attributes after
StringEscapeUtils.escapeHtml4,
+ which encodes double quotes but NOT single quotes - the attributes must
therefore be double-quoted
+ */
+ public static final String SCRIPT_TAG = "<script src=\"%s\"></script>\n";
+ public static final String LINK_TAG = "<link rel='stylesheet'
type='text/css' href=\"%s\">";
+
+ /*
+ request attribute through which the Felix Web Console exposes its root
path; referenced by name so
+ that this plugin keeps no wiring to the Felix Web Console APIs (see the
comment on the constants above)
+ */
+ private static final String ATTR_APP_ROOT = "felix.webconsole.appRoot";
@Reference(target =
"(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
private XSSFilter xssFilter;
@@ -99,8 +109,7 @@ public class XSSProtectionAPIWebConsolePlugin extends
HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse
response) {
String pluginResource = request.getPathInfo();
- String consoleRoot =
- request.getRequestURI().substring(0,
request.getRequestURI().indexOf(pluginResource));
+ String consoleRoot = getConsoleRoot(request);
if (CSS_RESOURCES.contains(pluginResource)) {
streamResource(response, FilenameUtils.getName(pluginResource),
"text/css");
} else if (JS_RESOURCES.contains(pluginResource)) {
@@ -122,7 +131,7 @@ public class XSSProtectionAPIWebConsolePlugin extends
HttpServlet {
printWriter.println("<li id='blocked-tab'><a
href='#blocked'><span>Status</span></a></li>");
if (xssFilter != null) {
printWriter.println(String.format(
- "<li id='config-tab'><a href='%s'><span>Active
Configuration</span></a></li>",
+ "<li id='config-tab'><a href=\"%s\"><span>Active
Configuration</span></a></li>",
escapedConsoleRoot + URI_CONFIG_XHR));
}
printWriter.println("</ul>");
@@ -146,6 +155,22 @@ public class XSSProtectionAPIWebConsolePlugin extends
HttpServlet {
}
}
+ /**
+ * Returns the web console root path. The value is taken from the {@code
felix.webconsole.appRoot} request
+ * attribute when the Felix Web Console provides it, with a fallback to
the container-provided context and
+ * servlet paths.
+ *
+ * @param request the request
+ * @return the web console root path
+ */
+ private static String getConsoleRoot(HttpServletRequest request) {
+ Object appRoot = request.getAttribute(ATTR_APP_ROOT);
+ if (appRoot instanceof String) {
+ return (String) appRoot;
+ }
+ return request.getContextPath() + request.getServletPath();
+ }
+
private void streamAntiSamyConfiguration(HttpServletResponse response) {
try {
response.setContentType("application/xml");
diff --git a/src/main/resources/webconsole/xss.js
b/src/main/resources/webconsole/xss.js
index 0f23552..30482cb 100644
--- a/src/main/resources/webconsole/xss.js
+++ b/src/main/resources/webconsole/xss.js
@@ -25,15 +25,18 @@ $(document).ready(function () {
success: function (data) {
if (data && data.hrefs) {
if (data.hrefs.length > 0) {
- var rows = '';
+ var tbody = $('#invalid-urls-rows');
+ tbody.empty();
for (var i = 0; i < data.hrefs.length; i++) {
var cssClass = (i % 2) === 0 ? 'even' : 'odd';
- rows += `<tr class="${cssClass}
ui-state-default">
- <td>${data.hrefs[i].href}</td>
- <td>${data.hrefs[i].times}</td>
- </tr>`;
+ // the blocked hrefs are attacker-controlled:
build the cells with
+ // text() (text nodes) instead of
interpolating them into markup,
+ // so recorded payloads cannot execute in the
console origin
+ tbody.append($('<tr></tr>')
+ .addClass(cssClass + '
ui-state-default')
+
.append($('<td></td>').text(data.hrefs[i].href))
+
.append($('<td></td>').text(String(data.hrefs[i].times))));
}
- $('#invalid-urls-rows').html(rows);
var table = $('#invalid-urls');
table.trigger('update');
var sorting = [[1, 1]];
diff --git
a/src/test/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePluginTest.java
b/src/test/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePluginTest.java
new file mode 100644
index 0000000..3bd2e1e
--- /dev/null
+++
b/src/test/java/org/apache/sling/xss/impl/webconsole/XSSProtectionAPIWebConsolePluginTest.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.webconsole;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class XSSProtectionAPIWebConsolePluginTest {
+
+ @Test
+ public void testConsoleRootIsNotDerivedFromTheRawRequestUri() throws
Exception {
+ XSSProtectionAPIWebConsolePlugin plugin = new
XSSProtectionAPIWebConsolePlugin();
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ // servlet containers strip path parameters (;name=value) before
servlet mapping, so a request with
+ // this URI still routes to the plugin; the raw URI must never be
reflected into the response, since
+ // escapeHtml4 does not encode single quotes
+
when(request.getRequestURI()).thenReturn("/system/console;v='onerror='alert(document.cookie)/xssprotection");
+ when(request.getPathInfo()).thenReturn("/xssprotection");
+ when(request.getContextPath()).thenReturn("");
+ when(request.getServletPath()).thenReturn("/system/console");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter output = new StringWriter();
+ when(response.getWriter()).thenReturn(new PrintWriter(output));
+
+ plugin.doGet(request, response);
+
+ String markup = output.toString();
+ assertFalse(markup.contains("onerror"), "Expected the raw request URI
to not be reflected into markup.");
+ assertTrue(
+ markup.contains("<script
src=\"/system/console/xssprotection/webconsole/xss.js\"></script>"),
+ "Expected the script tag to use the container-provided console
root in a double-quoted attribute.");
+ }
+
+ @Test
+ public void testConsoleRootPrefersTheFelixAppRootAttribute() throws
Exception {
+ XSSProtectionAPIWebConsolePlugin plugin = new
XSSProtectionAPIWebConsolePlugin();
+ HttpServletRequest request = mock(HttpServletRequest.class);
+
when(request.getAttribute("felix.webconsole.appRoot")).thenReturn("/ctx/system/console");
+ when(request.getPathInfo()).thenReturn("/xssprotection");
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter output = new StringWriter();
+ when(response.getWriter()).thenReturn(new PrintWriter(output));
+
+ plugin.doGet(request, response);
+
+ assertTrue(
+ output.toString()
+ .contains("<script
src=\"/ctx/system/console/xssprotection/webconsole/xss.js\"></script>"),
+ "Expected the script tag to use the Felix Web Console appRoot
request attribute.");
+ }
+}