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