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

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


The following commit(s) were added to refs/heads/main by this push:
     new 8703bcfac2 Add attributes to Context to control decoding 
RequestDispatcher path
8703bcfac2 is described below

commit 8703bcfac23003604ec82f03cf4c9d9452f95c32
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Thu Jan 23 12:08:56 2025 +0000

    Add attributes to Context to control decoding RequestDispatcher path
    
    Adds encodedReverseSolidusHandling and encodedSolidusHandling. Both
    default to DECODE (the current behaviour)
---
 java/org/apache/catalina/Context.java              |  67 +++++++++
 .../apache/catalina/core/ApplicationContext.java   |   3 +-
 java/org/apache/catalina/core/StandardContext.java |  41 ++++++
 .../apache/tomcat/util/buf/LocalStrings.properties |   2 +
 java/org/apache/tomcat/util/buf/UDecoder.java      |  99 +++++++++++--
 ...estApplicationContextGetRequestDispatcherC.java | 161 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   6 +
 webapps/docs/config/context.xml                    |  40 +++++
 8 files changed, 404 insertions(+), 15 deletions(-)

diff --git a/java/org/apache/catalina/Context.java 
b/java/org/apache/catalina/Context.java
index 9c59c10a9c..9cc19c5369 100644
--- a/java/org/apache/catalina/Context.java
+++ b/java/org/apache/catalina/Context.java
@@ -25,6 +25,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import jakarta.servlet.RequestDispatcher;
 import jakarta.servlet.ServletContainerInitializer;
 import jakarta.servlet.ServletContext;
 import jakarta.servlet.ServletRegistration;
@@ -36,6 +37,7 @@ import org.apache.catalina.deploy.NamingResourcesImpl;
 import org.apache.tomcat.ContextBind;
 import org.apache.tomcat.InstanceManager;
 import org.apache.tomcat.JarScanner;
+import org.apache.tomcat.util.buf.EncodedSolidusHandling;
 import org.apache.tomcat.util.descriptor.web.ApplicationParameter;
 import org.apache.tomcat.util.descriptor.web.ErrorPage;
 import org.apache.tomcat.util.descriptor.web.FilterDef;
@@ -1872,4 +1874,69 @@ public interface Context extends Container, ContextBind {
         return ConfigFileLoader.getSource().getResource(name);
     }
 
+
+    /**
+     * Obtain the current configuration for the handling of encoded reverse 
solidus (%5c - \) characters in paths used
+     * to obtain {@link RequestDispatcher} instances for this {@link Context}.
+     *
+     * @return Obtain the current configuration for the handling of encoded 
reverse solidus characters
+     */
+    default String getEncodedReverseSolidusHandling() {
+        return EncodedSolidusHandling.DECODE.getValue();
+    }
+
+
+    /**
+     * Configure the handling for encoded reverse solidus (%5c - \) characters 
in paths used to obtain
+     * {@link RequestDispatcher} instances for this {@link Context}.
+     *
+     * @param encodedReverseSolidusHandling One of the values of {@link 
EncodedSolidusHandling}
+     */
+    default void setEncodedReverseSolidusHandling(String 
encodedReverseSolidusHandling) {
+        throw new UnsupportedOperationException();
+    }
+
+
+    /**
+     * Obtain the current configuration for the handling of encoded reverse 
solidus (%5c - \) characters in paths used
+     * to obtain {@link RequestDispatcher} instances for this {@link Context}.
+     *
+     * @return Obtain the current configuration for the handling of encoded 
reverse solidus characters
+     */
+    default EncodedSolidusHandling getEncodedReverseSolidusHandlingEnum() {
+        return EncodedSolidusHandling.DECODE;
+    }
+
+
+    /**
+     * Obtain the current configuration for the handling of encoded solidus 
(%2f - /) characters in paths used to obtain
+     * {@link RequestDispatcher} instances for this {@link Context}.
+     *
+     * @return Obtain the current configuration for the handling of encoded 
solidus characters
+     */
+    default String getEncodedSolidusHandling() {
+        return EncodedSolidusHandling.DECODE.getValue();
+    }
+
+
+    /**
+     * Configure the handling for encoded solidus (%2f - /) characters in 
paths used to obtain {@link RequestDispatcher}
+     * instances for this {@link Context}.
+     *
+     * @param encodedSolidusHandling One of the values of {@link 
EncodedSolidusHandling}
+     */
+    default void setEncodedSolidusHandling(String encodedSolidusHandling) {
+        throw new UnsupportedOperationException();
+    }
+
+
+    /**
+     * Obtain the current configuration for the handling of encoded solidus 
(%2f - /) characters in paths used to obtain
+     * {@link RequestDispatcher} instances for this {@link Context}.
+     *
+     * @return Obtain the current configuration for the handling of encoded 
solidus characters
+     */
+    default EncodedSolidusHandling getEncodedSolidusHandlingEnum() {
+        return EncodedSolidusHandling.DECODE;
+    }
 }
diff --git a/java/org/apache/catalina/core/ApplicationContext.java 
b/java/org/apache/catalina/core/ApplicationContext.java
index 353a16b597..5fc0ea14e0 100644
--- a/java/org/apache/catalina/core/ApplicationContext.java
+++ b/java/org/apache/catalina/core/ApplicationContext.java
@@ -378,7 +378,8 @@ public class ApplicationContext implements ServletContext {
 
         // Decode only if the uri derived from the provided path is expected 
to be encoded
         if (getContext().getDispatchersUseEncodedPaths()) {
-            uriToMap = UDecoder.URLDecode(uriToMap, StandardCharsets.UTF_8);
+            uriToMap = UDecoder.URLDecode(uriToMap, StandardCharsets.UTF_8, 
context.getEncodedSolidusHandlingEnum(),
+                    context.getEncodedReverseSolidusHandlingEnum());
         }
 
         // Then normalize
diff --git a/java/org/apache/catalina/core/StandardContext.java 
b/java/org/apache/catalina/core/StandardContext.java
index 2994082059..a06014d052 100644
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -114,6 +114,7 @@ import org.apache.tomcat.InstanceManager;
 import org.apache.tomcat.InstanceManagerBindings;
 import org.apache.tomcat.JarScanner;
 import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.EncodedSolidusHandling;
 import org.apache.tomcat.util.buf.StringUtils;
 import org.apache.tomcat.util.compat.JreCompat;
 import org.apache.tomcat.util.descriptor.XmlIdentifiers;
@@ -800,9 +801,49 @@ public class StandardContext extends ContainerBase 
implements Context, Notificat
 
     private int notFoundClassResourceCacheSize = 1000;
 
+    private EncodedSolidusHandling encodedReverseSolidusHandling = 
EncodedSolidusHandling.DECODE;
+
+    private EncodedSolidusHandling encodedSolidusHandling = 
EncodedSolidusHandling.DECODE;
+
 
     // ----------------------------------------------------- Context Properties
 
+    @Override
+    public String getEncodedReverseSolidusHandling() {
+        return encodedReverseSolidusHandling.getValue();
+    }
+
+
+    @Override
+    public void setEncodedReverseSolidusHandling(String 
encodedReverseSolidusHandling) {
+        this.encodedReverseSolidusHandling = 
EncodedSolidusHandling.fromString(encodedReverseSolidusHandling);
+    }
+
+
+    @Override
+    public EncodedSolidusHandling getEncodedReverseSolidusHandlingEnum() {
+        return encodedReverseSolidusHandling;
+    }
+
+
+    @Override
+    public String getEncodedSolidusHandling() {
+        return encodedSolidusHandling.getValue();
+    }
+
+
+    @Override
+    public void setEncodedSolidusHandling(String encodedSolidusHandling) {
+        this.encodedSolidusHandling = 
EncodedSolidusHandling.fromString(encodedSolidusHandling);
+    }
+
+
+    @Override
+    public EncodedSolidusHandling getEncodedSolidusHandlingEnum() {
+        return encodedSolidusHandling;
+    }
+
+
     public int getNotFoundClassResourceCacheSize() {
         return notFoundClassResourceCacheSize;
     }
diff --git a/java/org/apache/tomcat/util/buf/LocalStrings.properties 
b/java/org/apache/tomcat/util/buf/LocalStrings.properties
index 90bf911655..c336015ddb 100644
--- a/java/org/apache/tomcat/util/buf/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/buf/LocalStrings.properties
@@ -44,3 +44,5 @@ uDecoder.noBackslash=The encoded backslash character is not 
allowed
 uDecoder.noSlash=The encoded slash character is not allowed
 uDecoder.urlDecode.conversionError=Failed to decode [{0}] using character set 
[{1}]
 uDecoder.urlDecode.missingDigit=Failed to decode [{0}] because the % character 
must be followed by two hexadecimal digits
+uDecoder.urlDecode.rejectEncodedReverseSolidus=Failed to decode [{0}] because 
it contained a %5c sequence the decoder is configured to reject
+uDecoder.urlDecode.rejectEncodedSolidus=Failed to decode [{0}] because it 
contained a %2f sequence the decoder is configured to reject
diff --git a/java/org/apache/tomcat/util/buf/UDecoder.java 
b/java/org/apache/tomcat/util/buf/UDecoder.java
index 0cc1c1bd08..23156b29f3 100644
--- a/java/org/apache/tomcat/util/buf/UDecoder.java
+++ b/java/org/apache/tomcat/util/buf/UDecoder.java
@@ -151,20 +151,20 @@ public final class UDecoder {
                         }
                     }
                 } else if (res == '\\') {
-                        switch (encodedReverseSolidusHandling) {
-                            case DECODE: {
-                                buff[idx] = (byte) res;
-                                break;
-                            }
-                            case REJECT: {
-                                throw EXCEPTION_BACKSLASH;
-                            }
-                            case PASS_THROUGH: {
-                                buff[idx++] = buff[j - 2];
-                                buff[idx++] = buff[j - 1];
-                                buff[idx] = buff[j];
-                            }
+                    switch (encodedReverseSolidusHandling) {
+                        case DECODE: {
+                            buff[idx] = (byte) res;
+                            break;
+                        }
+                        case REJECT: {
+                            throw EXCEPTION_BACKSLASH;
                         }
+                        case PASS_THROUGH: {
+                            buff[idx++] = buff[j - 2];
+                            buff[idx++] = buff[j - 1];
+                            buff[idx] = buff[j];
+                        }
+                    }
                 } else if (res == '%') {
                     /*
                      * If encoded '/' or '\' is going to be left encoded then 
so must encoded '%' else the subsequent
@@ -200,6 +200,24 @@ public final class UDecoder {
      * @exception IllegalArgumentException if a '%' character is not followed 
by a valid 2-digit hexadecimal number
      */
     public static String URLDecode(String str, Charset charset) {
+        return URLDecode(str, charset, EncodedSolidusHandling.DECODE, 
EncodedSolidusHandling.DECODE);
+    }
+
+
+    /**
+     * Decode and return the specified URL-encoded String. It is assumed the 
string is not a query string.
+     *
+     * @param str                           The url-encoded string
+     * @param charset                       The character encoding to use; if 
null, UTF-8 is used.
+     * @param encodedSolidusHandling        The required handling of encoded 
solidus (%2f - /)
+     * @param encodedReverseSolidusHandling The required handling of encoded 
reverse solidus (%5c - \)
+     *
+     * @return the decoded string
+     *
+     * @exception IllegalArgumentException if a '%' character is not followed 
by a valid 2-digit hexadecimal number
+     */
+    public static String URLDecode(String str, Charset charset, 
EncodedSolidusHandling encodedSolidusHandling,
+            EncodedSolidusHandling encodedReverseSolidusHandling) {
         if (str == null) {
             return null;
         }
@@ -248,7 +266,60 @@ public final class UDecoder {
                     char c1 = sourceChars[ix++];
                     char c2 = sourceChars[ix++];
                     if (isHexDigit(c1) && isHexDigit(c2)) {
-                        baos.write(x2c(c1, c2));
+                        int decoded = x2c(c1, c2);
+                        switch (decoded) {
+                            case '/': {
+                                switch (encodedSolidusHandling) {
+                                    case DECODE: {
+                                        osw.append('/');
+                                        break;
+                                    }
+                                    case PASS_THROUGH: {
+                                        osw.append(c);
+                                        osw.append(c1);
+                                        osw.append(c2);
+                                        break;
+                                    }
+                                    case REJECT: {
+                                        throw new IllegalArgumentException(
+                                                
sm.getString("uDecoder.urlDecode.rejectEncodedSolidus", str));
+                                    }
+                                }
+                                break;
+                            }
+                            case '\\': {
+                                switch (encodedReverseSolidusHandling) {
+                                    case DECODE: {
+                                        osw.append('\\');
+                                        break;
+                                    }
+                                    case PASS_THROUGH: {
+                                        osw.append(c);
+                                        osw.append(c1);
+                                        osw.append(c2);
+                                        break;
+                                    }
+                                    case REJECT: {
+                                        throw new IllegalArgumentException(
+                                                
sm.getString("uDecoder.urlDecode.rejectEncodedReverseSolidus", str));
+                                    }
+                                }
+                                break;
+                            }
+                            case '%': {
+                                if (encodedReverseSolidusHandling == 
EncodedSolidusHandling.PASS_THROUGH ||
+                                        encodedSolidusHandling == 
EncodedSolidusHandling.PASS_THROUGH) {
+                                    osw.append(c);
+                                    osw.append(c1);
+                                    osw.append(c2);
+                                } else {
+                                    baos.write('%');
+                                }
+                                break;
+                            }
+                            default:
+                                baos.write(decoded);
+                        }
                     } else {
                         throw new 
IllegalArgumentException(sm.getString("uDecoder.urlDecode.missingDigit", str));
                     }
diff --git 
a/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
 
b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
new file mode 100644
index 0000000000..ff5fd02d21
--- /dev/null
+++ 
b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
@@ -0,0 +1,161 @@
+/*
+ * 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.catalina.core;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+
+/*
+ * Tests interaction of ServletContext.getRequestDispatcher() and the 
Connector/Context attributes
+ * encodedSolidusHandling and encodedReverseSolidusHandling.
+ */
+@RunWith(value = Parameterized.class)
+public class TestApplicationContextGetRequestDispatcherC extends 
TomcatBaseTest {
+
+    @Parameters(name = "{index}: pathInfoRequest[{0}], 
pathInfoDispatcher[{1}]")
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+            // Defaults
+            { "/a/b/c", "/a/b/c", null, null },
+            { "/a%2fb%5cc/%25", "/a/b/c/%", "decode", "decode" },
+            { "/a%2fb%5cc/%25", "/a/b%5cc/%25", "decode", "passthrough" },
+            { "/a%2fb%5cc/%25", "/a%2fb/c/%25", "passthrough", "decode" },
+            { "/a%2fb%5cc/%25", "/a%2fb%5cc/%25", "passthrough", "passthrough" 
},
+        });
+    }
+
+
+    @Parameter(0)
+    public String pathInfoRequest;
+    @Parameter(1)
+    public String pathInfoDispatcher;
+    @Parameter(2)
+    public String encodedSolidusHandling;
+    @Parameter(3)
+    public String encodedReverseSolidusHandling;
+
+
+    @Test
+    public void testSomething() throws Exception {
+        doTest();
+    }
+
+
+    private void doTest() throws Exception {
+        // Setup Tomcat instance
+        Tomcat tomcat = getTomcatInstance();
+
+        // Configure the Connector
+        tomcat.getConnector().setAllowBackslash(true);
+        if (encodedSolidusHandling != null) {
+            
tomcat.getConnector().setEncodedSolidusHandling(encodedSolidusHandling);
+        }
+        if (encodedReverseSolidusHandling != null) {
+            
tomcat.getConnector().setEncodedReverseSolidusHandling(encodedReverseSolidusHandling);
+        }
+
+        // No file system docBase required
+        Context ctx = tomcat.addContext("/test", null);
+        ctx.addWelcomeFile("index.html");
+        if (encodedSolidusHandling != null) {
+            ctx.setEncodedSolidusHandling(encodedSolidusHandling);
+        }
+        if (encodedReverseSolidusHandling != null) {
+            
ctx.setEncodedReverseSolidusHandling(encodedReverseSolidusHandling);
+        }
+
+        // Servlet that performs a dispatch to ...
+        Tomcat.addServlet(ctx, "rd", new Dispatch(pathInfoRequest));
+        ctx.addServletMappingDecoded("/dispatch/*", "rd");
+
+        // ... this Servlet
+        Tomcat.addServlet(ctx, "target", new Target());
+        ctx.addServletMappingDecoded("/target/*", "target");
+
+        tomcat.start();
+
+        StringBuilder url = new StringBuilder("http://localhost:";);
+        url.append(getPort());
+        url.append("/test/dispatch");
+        url.append(pathInfoRequest);
+
+        ByteChunk bc = getUrl(url.toString());
+        String body = bc.toString();
+
+        Assert.assertEquals(pathInfoDispatcher + pathInfoDispatcher, body);
+    }
+
+
+    private static class Dispatch extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        private final String dispatchPath;
+
+        Dispatch(String dispatchPath) {
+            this.dispatchPath = dispatchPath;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            /*
+             * The original pathInfo as presented by the client and the 
dispatchPath are currently the same for this
+             * test, as is the handling configured in the Connector and 
Context. The test checks both the normal request
+             * processing and the dispatch processing for the correct handling 
of %2f and %5c. The test could be
+             * expanded so different settings are used for the Connector and 
the Context.
+             */
+            // Handle an edge case - the application needs to encode any paths 
used to obtain a RequestDispatcher
+            String pathInfo = req.getPathInfo();
+            if (pathInfo.endsWith("%")) {
+                pathInfo = pathInfo.replace("%", "%25");
+            }
+            req.getServletContext().getRequestDispatcher("/target" + pathInfo 
+ dispatchPath).forward(req, resp);
+        }
+    }
+
+
+    private static class Target extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            resp.getWriter().print(req.getPathInfo());
+        }
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 005588a961..3f2f3b030f 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -188,6 +188,12 @@
         the processing of the provided path is consistent with normal request
         processing. (markt)
       </scode>
+      <add>
+        Add <code>encodedReverseSolidusHandling</code> and
+        <code>encodedSolidusHandling</code> attributes to Context to provide
+        control over the handling of the path used to created a
+        <code>RequestDispatcher</code>. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml
index 69cbda5150..cf6fa6936d 100644
--- a/webapps/docs/config/context.xml
+++ b/webapps/docs/config/context.xml
@@ -409,6 +409,46 @@
         sufficient.</p>
       </attribute>
 
+      <attribute name="encodedReverseSolidusHandling" required="false">
+        <p>When set to <code>reject</code>, an attempt to obtain a
+        <code>RequestDispatcher</code> for a path containing a <code>%5c</code>
+        sequence will trigger an <code>IllegalArgumentException</code>. When 
set
+        to <code>decode</code>, any <code>%5c</code> sequence in the path used
+        to obtain a <code>RequestDispatcher</code> will have that sequence
+        decoded to <code>\</code> (and then normalized to <code>/</code>) 
before
+        the <code>RequestDispatcher</code> is created. When set to
+        <code>passthrough</code>, any <code>%5c</code> sequence in the path 
used
+        to obtain a <code>RequestDispatcher</code> will remain unchanged.</p>
+        <p>If <code>passthrough</code> is used then it is the application's
+        resposibility to perform any further <code>%nn</code> decoding 
required.
+        Any <code>%25</code> sequences (encoded <code>%</code>) in the path 
will
+        also be processed with the <code>%25</code> sequence unchanged
+        to avoid potential corruption and/or decoding failure when the path is
+        subsequently <code>%nn</code> decoded by the application.</p>
+        <p>If not specified, the default value is <code>decode</code>.</p>
+      </attribute>
+
+      <attribute name="encodedSolidusHandling" required="false">
+        <p>When set to <code>reject</code>, an attempt to obtain a
+        <code>RequestDispatcher</code> for a path containing a <code>%2f</code>
+        sequence will trigger an <code>IllegalArgumentException</code>. When 
set
+        to <code>decode</code>, any <code>%2f</code> sequence in the path used
+        to obtain a <code>RequestDispatcher</code> will have that sequence
+        decoded to <code>/</code> before the <code>RequestDispatcher</code> is
+        created. When set to <code>passthrough</code>, any <code>%2f</code>
+        sequence in the path used to obtain a <code>RequestDispatcher</code>
+        will remain unchanged.</p>
+        <p>If <code>passthrough</code> is used then it is the application's
+        resposibility to perform any further <code>%nn</code> decoding 
required.
+        Any <code>%25</code> sequences (encoded <code>%</code>) in the path 
will
+        also be processed with the <code>%25</code> sequence unchanged
+        to avoid potential corruption and/or decoding failure when the path is
+        subsequently <code>%nn</code> decoded by the application.</p>
+        <p>If not specified, the default value is <code>decode</code>. This
+        default will change to <code>reject</code> (to align with the
+        <strong>Connector</strong>) in Tomcat 12.</p>
+      </attribute>
+
       <attribute name="failCtxIfServletStartFails" required="false">
         <p>Set to <code>true</code> to have the context fail its startup if any
         servlet that has load-on-startup &gt;=0 fails its own startup.</p>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to