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 79d5e047aa Add support for encodedReverseSolidusHandling
79d5e047aa is described below

commit 79d5e047aa69cc0b899ae32fa7e6492675f792fa
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Jan 21 16:43:38 2025 +0000

    Add support for encodedReverseSolidusHandling
    
    Defaults to the current behaviour which is decode (so allowBackslash
    then controls what happens next).
---
 java/org/apache/catalina/connector/Connector.java  |  23 ++-
 .../apache/catalina/connector/CoyoteAdapter.java   |   3 +-
 .../apache/tomcat/util/buf/LocalStrings.properties |   1 +
 java/org/apache/tomcat/util/buf/UDecoder.java      |  72 +++++---
 test/org/apache/tomcat/util/buf/TestUDecoder.java  | 189 ++++++++++++++++++++-
 webapps/docs/changelog.xml                         |   8 +
 webapps/docs/config/ajp.xml                        |  21 +++
 webapps/docs/config/http.xml                       |  21 +++
 8 files changed, 316 insertions(+), 22 deletions(-)

diff --git a/java/org/apache/catalina/connector/Connector.java 
b/java/org/apache/catalina/connector/Connector.java
index f2c6df6635..7947cf0e06 100644
--- a/java/org/apache/catalina/connector/Connector.java
+++ b/java/org/apache/catalina/connector/Connector.java
@@ -270,7 +270,13 @@ public class Connector extends LifecycleMBeanBase {
 
 
     /**
-     * The behavior when an encoded solidus (slash) is submitted.
+     * The behavior when an encoded reverse solidus (backslash - \) is 
submitted.
+     */
+    private EncodedSolidusHandling encodedReverseSolidusHandling = 
EncodedSolidusHandling.DECODE;
+
+
+    /**
+     * The behavior when an encoded solidus (slash - /) is submitted.
      */
     private EncodedSolidusHandling encodedSolidusHandling = 
EncodedSolidusHandling.REJECT;
 
@@ -866,6 +872,21 @@ public class Connector extends LifecycleMBeanBase {
     }
 
 
+    public String getEncodedReverseSolidusHandling() {
+        return encodedReverseSolidusHandling.getValue();
+    }
+
+
+    public void setEncodedReverseSolidusHandling(String 
encodedReverseSolidusHandling) {
+        this.encodedReverseSolidusHandling = 
EncodedSolidusHandling.fromString(encodedReverseSolidusHandling);
+    }
+
+
+    public EncodedSolidusHandling getEncodedReverseSolidusHandlingInternal() {
+        return encodedReverseSolidusHandling;
+    }
+
+
     public String getEncodedSolidusHandling() {
         return encodedSolidusHandling.getValue();
     }
diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java 
b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 5149e7e0db..14b8294979 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -630,7 +630,8 @@ public class CoyoteAdapter implements Adapter {
                 // %xx decoding of the URL
                 try {
                     req.getURLDecoder().convert(decodedURI.getByteChunk(),
-                            connector.getEncodedSolidusHandlingInternal());
+                            connector.getEncodedSolidusHandlingInternal(),
+                            
connector.getEncodedReverseSolidusHandlingInternal());
                 } catch (IOException ioe) {
                     response.sendError(400, 
sm.getString("coyoteAdapter.invalidURIWithMessage", ioe.getMessage()));
                 }
diff --git a/java/org/apache/tomcat/util/buf/LocalStrings.properties 
b/java/org/apache/tomcat/util/buf/LocalStrings.properties
index ce64e0df22..90bf911655 100644
--- a/java/org/apache/tomcat/util/buf/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/buf/LocalStrings.properties
@@ -40,6 +40,7 @@ toStringUtil.classpath.unknown=Unknown - not an instance of 
URLClassLoader
 
 uDecoder.eof=End of file (EOF)
 uDecoder.isHexDigit=The hexadecimal encoding is invalid
+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
diff --git a/java/org/apache/tomcat/util/buf/UDecoder.java 
b/java/org/apache/tomcat/util/buf/UDecoder.java
index b11965095e..3a1fa6edbf 100644
--- a/java/org/apache/tomcat/util/buf/UDecoder.java
+++ b/java/org/apache/tomcat/util/buf/UDecoder.java
@@ -58,10 +58,12 @@ public final class UDecoder {
     /** %-encoded slash is forbidden in resource path */
     private static final IOException EXCEPTION_SLASH = new 
DecodeException(sm.getString("uDecoder.noSlash"));
 
+    /** %-encoded backslash is forbidden in resource path */
+    private static final IOException EXCEPTION_BACKSLASH = new 
DecodeException(sm.getString("uDecoder.noBackslash"));
 
     /**
      * URLDecode, will modify the source. Assumes source bytes are encoded 
using a superset of US-ASCII as per RFC 7230.
-     * "%2f" will be rejected unless the input is a query string.
+     * "%5c" will be decoded. "%2f" will be rejected unless the input is a 
query string.
      *
      * @param mb    The URL encoded bytes
      * @param query {@code true} if this is a query string. For a query string 
'+' will be decoded to ' '
@@ -70,9 +72,9 @@ public final class UDecoder {
      */
     public void convert(ByteChunk mb, boolean query) throws IOException {
         if (query) {
-            convert(mb, true, EncodedSolidusHandling.DECODE);
+            convert(mb, true, EncodedSolidusHandling.DECODE, 
EncodedSolidusHandling.DECODE);
         } else {
-            convert(mb, false, EncodedSolidusHandling.REJECT);
+            convert(mb, false, EncodedSolidusHandling.REJECT, 
EncodedSolidusHandling.DECODE);
         }
     }
 
@@ -85,14 +87,35 @@ public final class UDecoder {
      *                                   parameter will be ignored and the %2f 
sequence will be decoded
      *
      * @throws IOException Invalid %xx URL encoding
+     *
+     * @deprecated Unused. Will be removed in Tomcat 12. Use
+     *                 {@link #convert(ByteChunk, EncodedSolidusHandling, 
EncodedSolidusHandling)}
      */
+    @Deprecated
     public void convert(ByteChunk mb, EncodedSolidusHandling 
encodedSolidusHandling) throws IOException {
-        convert(mb, false, encodedSolidusHandling);
+        convert(mb, false, encodedSolidusHandling, 
EncodedSolidusHandling.DECODE);
     }
 
 
-    private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling 
encodedSolidusHandling)
-            throws IOException {
+    /**
+     * URLDecode, will modify the source. Assumes source bytes are encoded 
using a superset of US-ASCII as per RFC 7230.
+     *
+     * @param mb                            The URL encoded bytes
+     * @param encodedSolidusHandling        How should the %2f sequence 
handled by the decoder? For query strings this
+     *                                          parameter will be ignored and 
the %2f sequence will be decoded
+     * @param encodedReverseSolidusHandling How should the %5c sequence 
handled by the decoder? For query strings this
+     *                                          parameter will be ignored and 
the %5c sequence will be decoded
+     *
+     * @throws IOException Invalid %xx URL encoding
+     */
+    public void convert(ByteChunk mb, EncodedSolidusHandling 
encodedSolidusHandling,
+            EncodedSolidusHandling encodedReverseSolidusHandling) throws 
IOException {
+        convert(mb, false, encodedSolidusHandling, 
encodedReverseSolidusHandling);
+    }
+
+
+    private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling 
encodedSolidusHandling,
+            EncodedSolidusHandling encodedReverseSolidusHandling) throws 
IOException {
 
         int start = mb.getStart();
         byte buff[] = mb.getBytes();
@@ -145,22 +168,33 @@ public final class UDecoder {
                             buff[idx] = buff[j];
                         }
                     }
+                } 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];
+                            }
+                        }
                 } else if (res == '%') {
                     /*
-                     * If encoded '/' is going to be left encoded then so must 
encoded '%' else the subsequent %nn
-                     * decoding will either fail or corrupt the output.
+                     * If encoded '/' or '\' is going to be left encoded then 
so must encoded '%' else the subsequent
+                     * %nn decoding will either fail or corrupt the output.
                      */
-                    switch (encodedSolidusHandling) {
-                        case DECODE:
-                        case REJECT: {
-                            buff[idx] = (byte) res;
-                            break;
-                        }
-                        case PASS_THROUGH: {
-                            buff[idx++] = buff[j - 2];
-                            buff[idx++] = buff[j - 1];
-                            buff[idx] = buff[j];
-                        }
+                    if 
(encodedSolidusHandling.equals(EncodedSolidusHandling.PASS_THROUGH) ||
+                            
encodedReverseSolidusHandling.equals(EncodedSolidusHandling.PASS_THROUGH)) {
+                        buff[idx++] = buff[j - 2];
+                        buff[idx++] = buff[j - 1];
+                        buff[idx] = buff[j];
+                    } else {
+                        buff[idx] = (byte) res;
                     }
                 } else {
                     buff[idx] = (byte) res;
diff --git a/test/org/apache/tomcat/util/buf/TestUDecoder.java 
b/test/org/apache/tomcat/util/buf/TestUDecoder.java
index 94dbd14fb6..3fcecd0852 100644
--- a/test/org/apache/tomcat/util/buf/TestUDecoder.java
+++ b/test/org/apache/tomcat/util/buf/TestUDecoder.java
@@ -276,7 +276,194 @@ public class TestUDecoder {
         bc.setCharset(StandardCharsets.UTF_8);
 
         UDecoder udecoder = new UDecoder();
-        udecoder.convert(bc, solidusHandling);
+        udecoder.convert(bc, solidusHandling, EncodedSolidusHandling.DECODE);
+
+        return bc.toString();
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus01() throws IOException {
+        doTestReverseSolidus("xxxxxx", "xxxxxx");
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus02() throws IOException {
+        doTestReverseSolidus("%20xxxx", " xxxx");
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus03() throws IOException {
+        doTestReverseSolidus("xx%20xx", "xx xx");
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus04() throws IOException {
+        doTestReverseSolidus("xxxx%20", "xxxx ");
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus05a() throws IOException {
+        doTestReverseSolidus("%5cxxxx", EncodedSolidusHandling.REJECT);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus05b() throws IOException {
+        String result = doTestReverseSolidus("%5cxxxx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("%5cxxxx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus05c() throws IOException {
+        String result = doTestReverseSolidus("%5cxxxx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("\\xxxx", result);
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus06a() throws IOException {
+        doTestReverseSolidus("%5cxx%20xx", EncodedSolidusHandling.REJECT);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus06b() throws IOException {
+        String result = doTestReverseSolidus("%5cxx%20xx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("%5cxx xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus06c() throws IOException {
+        String result = doTestReverseSolidus("%5cxx%20xx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("\\xx xx", result);
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus07a() throws IOException {
+        doTestReverseSolidus("xx%5c%20xx", EncodedSolidusHandling.REJECT);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus07b() throws IOException {
+        String result = doTestReverseSolidus("xx%5c%20xx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("xx%5c xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus07c() throws IOException {
+        String result = doTestReverseSolidus("xx%5c%20xx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("xx\\ xx", result);
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus08a() throws IOException {
+        doTestReverseSolidus("xx%20%5cxx", EncodedSolidusHandling.REJECT);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus08b() throws IOException {
+        String result = doTestReverseSolidus("xx%20%5cxx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("xx %5cxx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus08c() throws IOException {
+        String result = doTestReverseSolidus("xx%20%5cxx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("xx \\xx", result);
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus09a() throws IOException {
+        doTestReverseSolidus("xx%20xx%5c", EncodedSolidusHandling.REJECT);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus09b() throws IOException {
+        String result = doTestReverseSolidus("xx%20xx%5c", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("xx xx%5c", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus09c() throws IOException {
+        String result = doTestReverseSolidus("xx%20xx%5c", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("xx xx\\", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus10a() throws IOException {
+        String result = doTestReverseSolidus("xx%25xx", 
EncodedSolidusHandling.REJECT);
+        Assert.assertEquals("xx%xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus10b() throws IOException {
+        String result = doTestReverseSolidus("xx%25xx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("xx%25xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus10c() throws IOException {
+        String result = doTestReverseSolidus("xx%25xx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("xx%xx", result);
+    }
+
+
+    @Test(expected = CharConversionException.class)
+    public void testURLDecodeStringReverseSolidus11a() throws IOException {
+        String result = doTestReverseSolidus("xx%5c%25xx", 
EncodedSolidusHandling.REJECT);
+        Assert.assertEquals("xx%xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus11b() throws IOException {
+        String result = doTestReverseSolidus("xx%5c%25xx", 
EncodedSolidusHandling.PASS_THROUGH);
+        Assert.assertEquals("xx%5c%25xx", result);
+    }
+
+
+    @Test
+    public void testURLDecodeStringReverseSolidus11c() throws IOException {
+        String result = doTestReverseSolidus("xx%5c%25xx", 
EncodedSolidusHandling.DECODE);
+        Assert.assertEquals("xx\\%xx", result);
+    }
+
+
+    private void doTestReverseSolidus(String input, String expected) throws 
IOException {
+        for (EncodedSolidusHandling solidusHandling : 
EncodedSolidusHandling.values()) {
+            String result = doTestReverseSolidus(input, solidusHandling);
+            Assert.assertEquals(expected, result);
+        }
+    }
+
+
+    private String doTestReverseSolidus(String input, EncodedSolidusHandling 
reverseSolidusHandling) throws IOException {
+        byte[] b = input.getBytes(StandardCharsets.UTF_8);
+        ByteChunk bc = new ByteChunk(16);
+        bc.setBytes(b, 0,  b.length);
+        bc.setCharset(StandardCharsets.UTF_8);
+
+        UDecoder udecoder = new UDecoder();
+        udecoder.convert(bc, EncodedSolidusHandling.REJECT, 
reverseSolidusHandling);
 
         return bc.toString();
     }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index a6d35ada38..3c6726c3f2 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -225,6 +225,14 @@
         completed to aid GC and reduce the size of the HTTP/2 recycled request
         and response cache. (markt)
       </scode>
+      <add>
+        Add a new Connector configuration attribute,
+        <code>encodedReverseSolidusHandling</code>, to control how
+        <code>%5c</code> sequences in URLs are handled. The default behaviour 
is
+        unchanged (decode) keeping mind mind that the
+        <strong>allowBackslash</strong> attributes determines how the decoded
+        URI is processed. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/ajp.xml b/webapps/docs/config/ajp.xml
index 5e0225779b..9b297bb904 100644
--- a/webapps/docs/config/ajp.xml
+++ b/webapps/docs/config/ajp.xml
@@ -119,6 +119,27 @@
       By default, DNS lookups are disabled.</p>
     </attribute>
 
+    <attribute name="encodedReverseSolidusHandling" required="false">
+      <p>When set to <code>reject</code> request paths containing a
+      <code>%5c</code> sequence will be rejected with a 400 response. When set
+      to <code>decode</code> request paths containing a <code>%5c</code>
+      sequence will have that sequence decoded to <code>\</code> at the same
+      time other <code>%nn</code> sequences are decoded. When set to
+      <code>passthrough</code> request paths containing a <code>%5c</code>
+      sequence will be processed with the <code>%5c</code> sequence unchanged.
+      </p>
+      <p>When set to <code>decoded</code>, the <strong>allowBackslash</strong>
+      attribute will be applied after decoding.
+      </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 request
+      path with 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> request paths containing a
       <code>%2f</code> sequence will be rejected with a 400 response. When set
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index a94b3095dc..265ff47772 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -115,6 +115,27 @@
       By default, DNS lookups are disabled.</p>
     </attribute>
 
+    <attribute name="encodedReverseSolidusHandling" required="false">
+      <p>When set to <code>reject</code> request paths containing a
+      <code>%5c</code> sequence will be rejected with a 400 response. When set
+      to <code>decode</code> request paths containing a <code>%5c</code>
+      sequence will have that sequence decoded to <code>\</code> at the same
+      time other <code>%nn</code> sequences are decoded. When set to
+      <code>passthrough</code> request paths containing a <code>%5c</code>
+      sequence will be processed with the <code>%5c</code> sequence unchanged.
+      </p>
+      <p>When set to <code>decoded</code>, the <strong>allowBackslash</strong>
+      attribute will be applied after decoding.
+      </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 request
+      path with 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> request paths containing a
       <code>%2f</code> sequence will be rejected with a 400 response. When set


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

Reply via email to