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.");
+    }
+}

Reply via email to