Skip to content

Commit

Permalink
Add support for encodedReverseSolidusHandling
Browse files Browse the repository at this point in the history
Defaults to the current behaviour which is decode (so allowBackslash
then controls what happens next).
  • Loading branch information
markt-asf committed Jan 21, 2025
1 parent 4ed62bf commit 696a251
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 22 deletions.
23 changes: 22 additions & 1 deletion java/org/apache/catalina/connector/Connector.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,13 @@ public Connector(ProtocolHandler protocolHandler) {


/**
* 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;

Expand Down Expand Up @@ -866,6 +872,21 @@ public UpgradeProtocol[] findUpgradeProtocols() {
}


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();
}
Expand Down
3 changes: 2 additions & 1 deletion java/org/apache/catalina/connector/CoyoteAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,8 @@ protected boolean postParseRequest(org.apache.coyote.Request req, Request reques
// %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()));
}
Expand Down
1 change: 1 addition & 0 deletions java/org/apache/tomcat/util/buf/LocalStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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
72 changes: 53 additions & 19 deletions java/org/apache/tomcat/util/buf/UDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ public synchronized Throwable fillInStackTrace() {
/** %-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 ' '
Expand All @@ -70,9 +72,9 @@ public synchronized Throwable fillInStackTrace() {
*/
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);
}
}

Expand All @@ -85,14 +87,35 @@ public void convert(ByteChunk mb, boolean query) throws IOException {
* 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();
Expand Down Expand Up @@ -145,22 +168,33 @@ private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling encoded
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;
Expand Down
189 changes: 188 additions & 1 deletion test/org/apache/tomcat/util/buf/TestUDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,194 @@ private String doTestSolidus(String input, EncodedSolidusHandling solidusHandlin
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();
}
Expand Down
8 changes: 8 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,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">
Expand Down
Loading

0 comments on commit 696a251

Please sign in to comment.