This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 2ee6b5c9f1cbe8b675bbc79f5edde915a66df020
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Nov 3 16:07:39 2026 +0000

    Refactor AsyncContextImpl.dispatch() to better handle %nn encoded URIs
---
 .../org/apache/catalina/core/AsyncContextImpl.java | 29 +++------
 java/org/apache/catalina/util/RequestUtil.java     | 43 +++++++------
 .../apache/catalina/core/TestAsyncContextImpl.java | 71 ++++++++++++++++++++++
 3 files changed, 105 insertions(+), 38 deletions(-)

diff --git a/java/org/apache/catalina/core/AsyncContextImpl.java 
b/java/org/apache/catalina/core/AsyncContextImpl.java
index 57e238d0ed..655215eed3 100644
--- a/java/org/apache/catalina/core/AsyncContextImpl.java
+++ b/java/org/apache/catalina/core/AsyncContextImpl.java
@@ -18,7 +18,6 @@ package org.apache.catalina.core;
 
 import java.io.IOException;
 import java.io.Serial;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -42,7 +41,7 @@ import org.apache.catalina.Context;
 import org.apache.catalina.Host;
 import org.apache.catalina.Valve;
 import org.apache.catalina.connector.Request;
-import org.apache.catalina.util.URLEncoder;
+import org.apache.catalina.util.RequestUtil;
 import org.apache.coyote.ActionCode;
 import org.apache.coyote.AsyncContextCallback;
 import org.apache.coyote.RequestInfo;
@@ -220,29 +219,19 @@ public class AsyncContextImpl implements AsyncContext, 
AsyncContextCallback {
 
     @Override
     public void dispatch() {
-        String path;
-        String pathInfo;
         Request request = this.request;
         // Calls check() so local copies are validated
-        ServletRequest servletRequest = getRequest();
-        if (servletRequest instanceof HttpServletRequest sr) {
-            path = sr.getServletPath();
-            pathInfo = sr.getPathInfo();
-        } else {
-            path = request.getServletPath();
-            pathInfo = request.getPathInfo();
-        }
-        if (pathInfo != null && !pathInfo.isEmpty()) {
-            path = path + pathInfo;
+        HttpServletRequest hsr = getRequest() instanceof HttpServletRequest h 
? h : request;
+        String path = hsr.getRequestURI();
+        String cpath = 
RequestUtil.getContextPathEncoded(request.getConnector(), hsr);
+        if (cpath.length() > 1) {
+            path = path.substring(cpath.length());
         }
         /*
-         * This is a dispatch of the original request path. That path will 
always be URI-decoded. That will cause
-         * problems in ServletContext.getRequestDispatcher() if the decoded 
URI contains a literal '?' as it will be
-         * treated as a query delimiter. Therefore, ignore 
context.getDispatchersUseEncodedPaths() here and always
-         * encode. Also need to mark this request as ignoring 
context.getDispatchersUseEncodedPaths() so it is handled
-         * correctly in ServletContext.getRequestDispatcher().
+         * This is a dispatch of the original request path. That path 
(obtained from request.getRequestURI()) will
+         * always be URL-encoded. Therefore, ignore 
context.getDispatchersUseEncodedPaths() here and mark as always
+         * encoded so it is handled correctly in 
ServletContext.getRequestDispatcher().
          */
-        path = URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8);
         try {
             uriEncoded.set(Boolean.TRUE);
             dispatch(path);
diff --git a/java/org/apache/catalina/util/RequestUtil.java 
b/java/org/apache/catalina/util/RequestUtil.java
index 332e93afce..74ef26f423 100644
--- a/java/org/apache/catalina/util/RequestUtil.java
+++ b/java/org/apache/catalina/util/RequestUtil.java
@@ -22,7 +22,6 @@ import java.util.Enumeration;
 
 import jakarta.servlet.http.HttpServletRequest;
 
-import org.apache.catalina.Context;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.connector.Request;
 import org.apache.tomcat.util.buf.EncodedSolidusHandling;
@@ -208,32 +207,28 @@ public final class RequestUtil {
     }
 
 
-    public static String getContextPathUndecoded(Request request) {
+    /**
+     * Obtains the original, encoded, non-normalized (including sequences of 
multiple '/') context path as provided by
+     * the user agent.
+     *
+     * @param connector          The Connector on which the request was 
received
+     * @param httpServletRequest The request from which the original context 
path should be extracted
+     *
+     * @return The original, encoded, non-normalized context path
+     */
+    public static String getContextPathEncoded(Connector connector, 
HttpServletRequest httpServletRequest) {
 
-        int lastSlash = request.getMappingData().contextSlashCount;
+        String canonicalContextPath = 
httpServletRequest.getServletContext().getContextPath();
+        int lastSlash = slashCount(canonicalContextPath);
         // Special case handling for the root context
         if (lastSlash == 0) {
             return "";
         }
 
-        String uri = request.getRequestURI();
-        Context context = request.getContext();
-        String canonicalContextPath = 
request.getServletContext().getContextPath();
-        Connector connector = request.getConnector();
+        String uri = httpServletRequest.getRequestURI();
         EncodedSolidusHandling encodedSolidusHandling = 
connector.getEncodedSolidusHandlingInternal();
 
         int pos = 0;
-        if (!context.getAllowMultipleLeadingForwardSlashInPath()) {
-            // Ensure that the returned value only starts with a single '/'.
-            // This prevents the value being misinterpreted as a 
protocol-relative
-            // URI if used with sendRedirect().
-            do {
-                pos++;
-            } while (pos < uri.length() && uri.charAt(pos) == '/');
-            pos--;
-            uri = uri.substring(pos);
-        }
-
         char[] uriChars = uri.toCharArray();
         // Need at least the number of slashes in the context path
         while (lastSlash > 0) {
@@ -324,4 +319,16 @@ public final class RequestUtil {
         }
         return -1;
     }
+
+
+    private static int slashCount(String uri) {
+        int result = 0;
+        int start = 0;
+        while ((start = uri.indexOf('/', start)) > -1) {
+            start++;
+            result++;
+        }
+
+        return result;
+    }
 }
diff --git a/test/org/apache/catalina/core/TestAsyncContextImpl.java 
b/test/org/apache/catalina/core/TestAsyncContextImpl.java
index dadf500f06..fd1bb765dc 100644
--- a/test/org/apache/catalina/core/TestAsyncContextImpl.java
+++ b/test/org/apache/catalina/core/TestAsyncContextImpl.java
@@ -66,11 +66,13 @@ import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.catalina.valves.TesterAccessLogValve;
 import org.apache.catalina.valves.ValveBase;
+import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.unittest.TesterContext;
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.buf.EncodedSolidusHandling;
 import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
 import org.apache.tomcat.util.descriptor.web.ErrorPage;
+import org.apache.tomcat.util.scan.StandardJarScanner;
 import org.easymock.EasyMock;
 
 public class TestAsyncContextImpl extends TomcatBaseTest {
@@ -2520,6 +2522,73 @@ public class TestAsyncContextImpl extends TomcatBaseTest 
{
     }
 
 
+    /*
+     * A %3b in the request URI is an encoded semi-colon that is part of the 
path, not a path parameter delimiter. After
+     * AsyncContext.dispatch() the servlet should see exactly the same path 
info as it saw for the original request.
+     */
+    @Test
+    public void testAsyncDispatchEncodedSemicolon() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        Context context = tomcat.addContext("/test", null);
+        // Disable class path scanning - it slows the tests down by almost an 
order of magnitude
+        ((StandardJarScanner) context.getJarScanner()).setScanClassPath(false);
+
+        Wrapper w = Tomcat.addServlet(context, "semicolon", new 
AsyncDispatchPathInfoServlet());
+        w.setAsyncSupported(true);
+        context.addServletMapping("/semicolon/*", "semicolon");
+        tomcat.start();
+
+        ByteChunk responseBody = new ByteChunk();
+        int rc = getUrl("http://localhost:"; + getPort() + 
"/a/b/../../test/semicolon/foo%3bbar", responseBody, null);
+
+        Assert.assertEquals(200, rc);
+        // Same pathInfo before and after the dispatch
+        Assert.assertEquals("/foo;bar|/foo;bar", responseBody.toString());
+    }
+
+
+    /*
+     * Multiple leading slashes in the request URI must not break 
AsyncContext.dispatch(). The servlet should see
+     * exactly the same path info before and after the dispatch.
+     */
+    @Test
+    public void testAsyncDispatchMultipleLeadingSlashes() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+        Context context = tomcat.addContext("/test", null);
+        // Disable class path scanning - it slows the tests down by almost an 
order of magnitude
+        ((StandardJarScanner) context.getJarScanner()).setScanClassPath(false);
+
+        Wrapper w = Tomcat.addServlet(context, "slashes", new 
AsyncDispatchPathInfoServlet());
+        w.setAsyncSupported(true);
+        context.addServletMapping("/slashes/*", "slashes");
+        tomcat.start();
+
+        ByteChunk responseBody = new ByteChunk();
+        int rc = getUrl("http://localhost:"; + getPort() + 
"//test/slashes/foo", responseBody, null);
+
+        Assert.assertEquals(200, rc);
+        Assert.assertEquals("/foo|/foo", responseBody.toString());
+    }
+
+
+    private static class AsyncDispatchPathInfoServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            if (req.getAttribute("first") == null) {
+                req.setAttribute("first", req.getPathInfo());
+                AsyncContext ac = req.startAsync();
+                ac.dispatch();
+            } else {
+                resp.setContentType("text/plain");
+                resp.getWriter().print(req.getAttribute("first") + "|" + 
req.getPathInfo());
+            }
+        }
+    }
+
+
     private static class AsyncDispatchUrlWithSpacesServlet extends HttpServlet 
{
 
         private static final long serialVersionUID = 1L;
@@ -2666,6 +2735,8 @@ public class TestAsyncContextImpl extends TomcatBaseTest {
 
 
     private void doTestAsyncIoEnd(boolean useThread, boolean useComplete) 
throws Exception {
+        // Make sure the logger exists before setting the level
+        LogFactory.getLog("org.apache.coyote");
         
LogManager.getLogManager().getLogger("org.apache.coyote").setLevel(Level.ALL);
         try {
             Tomcat tomcat = getTomcatInstance();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to