File tomcat-9.0.36-CVE-2025-31651.patch of Package tomcat.39019
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/connector/CoyoteAdapter.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -649,21 +649,19 @@ public class CoyoteAdapter implements Ad
response.sendError(400, "Invalid URI");
}
} else {
- /* The URI is chars or String, and has been sent using an in-memory
- * protocol handler. The following assumptions are made:
- * - req.requestURI() has been set to the 'original' non-decoded,
- * non-normalized URI
- * - req.decodedURI() has been set to the decoded, normalized form
- * of req.requestURI()
+ /*
+ * The URI is chars or String, and has been sent using an in-memory protocol handler. The following
+ * assumptions are made:
+ *
+ * - req.requestURI() has been set to the 'original' non-decoded, non-normalized URI that includes path
+ * parameters (if any)
+ *
+ * - req.decodedURI() has been set to the decoded, normalized form of req.requestURI() with any path
+ * parameters removed
+ *
+ * - 'suspicious' URI filtering, if required, has already been performed
*/
decodedURI.toChars();
- // Remove all path parameters; any needed path parameter should be set
- // using the request object rather than passing it in the URL
- CharChunk uriCC = decodedURI.getCharChunk();
- int semicolon = uriCC.indexOf(';');
- if (semicolon > 0) {
- decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon);
- }
}
// Request mapping.
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/valves/rewrite/RewriteValve.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/valves/rewrite/RewriteValve.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/valves/rewrite/RewriteValve.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
+import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -68,6 +69,24 @@ import org.apache.tomcat.util.http.Reque
*/
public class RewriteValve extends ValveBase {
+ private static final URLEncoder REWRITE_DEFAULT_ENCODER;
+ private static final URLEncoder REWRITE_QUERY_ENCODER;
+
+ static {
+ /*
+ * See the detailed explanation of encoding/decoding during URL re-writing in the invoke() method.
+ *
+ * These encoders perform the second stage of encoding, after re-writing has completed. These rewrite specific
+ * encoders treat '%' as a safe character so that URLs and query strings already processed by encodeForRewrite()
+ * do not end up with double encoding of '%' characters.
+ */
+ REWRITE_DEFAULT_ENCODER = (URLEncoder) URLEncoder.DEFAULT.clone();
+ REWRITE_DEFAULT_ENCODER.addSafeCharacter('%');
+
+ REWRITE_QUERY_ENCODER = (URLEncoder) URLEncoder.QUERY.clone();
+ REWRITE_QUERY_ENCODER.addSafeCharacter('%');
+ }
+
/**
* The rewrite rules that the valve will use.
*/
@@ -313,14 +332,42 @@ public class RewriteValve extends ValveB
context ? request.getRequestPathMB() : request.getDecodedRequestURIMB();
urlMB.toChars();
CharSequence urlDecoded = urlMB.getCharChunk();
+
+ /*
+ * The URL presented to the rewrite valve is the URL that is used for request mapping. That URL has been
+ * processed to: remove path parameters; remove the query string; decode; and normalize the URL. It may
+ * contain literal '%', '?' and/or ';' characters at this point.
+ *
+ * The re-write rules need to be able to process URLs with literal '?' characters and add query strings
+ * without the two becoming confused. The re-write rules also need to be able to insert literal '%'
+ * characters without them being confused with %nn encoding.
+ *
+ * To meet these requirement, the URL is processed as follows.
+ *
+ * Step 1. The URL is partially re-encoded by encodeForRewrite(). This method encodes any literal '%', ';'
+ * and/or '?' characters in the URL using the standard %nn form.
+ *
+ * Step 2. The re-write processing runs with the provided re-write rules against the partially encoded URL.
+ * If a re-write rule needs to insert a literal '%', ';' or '?', it must do so in %nn encoded form.
+ *
+ * Step 3. The URL (and query string if present) is re-encoded using the re-write specific encoders
+ * (REWRITE_DEFAULT_ENCODER and REWRITE_QUERY_ENCODER) that behave the same was as the standard encoders
+ * apart from '%' being treated as a safe character. This prevents double encoding of any '%' characters
+ * present in the URL from steps 1 or 2.
+ */
+
+ // Step 1. Encode URL for processing by the re-write rules.
+ CharSequence urlRewriteEncoded = encodeForRewrite(urlDecoded);
CharSequence host = request.getServerName();
boolean rewritten = false;
boolean done = false;
boolean qsa = false;
boolean qsd = false;
+
+ // Step 2. Process the URL using the re-write rules.
for (int i = 0; i < rules.length; i++) {
RewriteRule rule = rules[i];
- CharSequence test = (rule.isHost()) ? host : urlDecoded;
+ CharSequence test = (rule.isHost()) ? host : urlRewriteEncoded;
CharSequence newtest = rule.evaluate(test, resolver);
if (newtest != null && !test.equals(newtest.toString())) {
if (containerLog.isDebugEnabled()) {
@@ -330,7 +377,7 @@ public class RewriteValve extends ValveB
if (rule.isHost()) {
host = newtest;
} else {
- urlDecoded = newtest;
+ urlRewriteEncoded = newtest;
}
rewritten = true;
}
@@ -363,29 +410,31 @@ public class RewriteValve extends ValveB
if (rule.isRedirect() && newtest != null) {
// Append the query string to the url if there is one and it
// hasn't been rewritten
- String urlStringDecoded = urlDecoded.toString();
- int index = urlStringDecoded.indexOf("?");
- String rewrittenQueryStringDecoded;
+ String urlStringRewriteEncoded = urlRewriteEncoded.toString();
+ int index = urlStringRewriteEncoded.indexOf('?');
+ String rewrittenQueryStringRewriteEncoded;
if (index == -1) {
- rewrittenQueryStringDecoded = null;
+ rewrittenQueryStringRewriteEncoded = null;
} else {
- rewrittenQueryStringDecoded = urlStringDecoded.substring(index + 1);
- urlStringDecoded = urlStringDecoded.substring(0, index);
+ rewrittenQueryStringRewriteEncoded = urlStringRewriteEncoded.substring(index + 1);
+ urlStringRewriteEncoded = urlStringRewriteEncoded.substring(0, index);
}
- StringBuffer urlStringEncoded =
- new StringBuffer(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset));
+ // Step 3. Complete the 2nd stage to encoding.
+ StringBuilder urlStringEncoded =
+ new StringBuilder(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset));
+
if (!qsd && originalQueryStringEncoded != null
&& originalQueryStringEncoded.length() > 0) {
- if (rewrittenQueryStringDecoded == null) {
+ if (rewrittenQueryStringRewriteEncoded == null) {
urlStringEncoded.append('?');
urlStringEncoded.append(originalQueryStringEncoded);
} else {
if (qsa) {
// if qsa is specified append the query
urlStringEncoded.append('?');
- urlStringEncoded.append(URLEncoder.QUERY.encode(
- rewrittenQueryStringDecoded, uriCharset));
+ urlStringEncoded.append(
+ REWRITE_QUERY_ENCODER.encode(rewrittenQueryStringRewriteEncoded, uriCharset));
urlStringEncoded.append('&');
urlStringEncoded.append(originalQueryStringEncoded);
} else if (index == urlStringEncoded.length() - 1) {
@@ -394,14 +443,14 @@ public class RewriteValve extends ValveB
urlStringEncoded.deleteCharAt(index);
} else {
urlStringEncoded.append('?');
- urlStringEncoded.append(URLEncoder.QUERY.encode(
- rewrittenQueryStringDecoded, uriCharset));
+ urlStringEncoded.append(
+ REWRITE_QUERY_ENCODER.encode(rewrittenQueryStringRewriteEncoded, uriCharset));
}
}
- } else if (rewrittenQueryStringDecoded != null) {
+ } else if (rewrittenQueryStringRewriteEncoded != null) {
urlStringEncoded.append('?');
- urlStringEncoded.append(
- URLEncoder.QUERY.encode(rewrittenQueryStringDecoded, uriCharset));
+ urlStringEncoded
+ .append(REWRITE_QUERY_ENCODER.encode(rewrittenQueryStringRewriteEncoded, uriCharset));
}
// Insert the context if
@@ -479,13 +528,15 @@ public class RewriteValve extends ValveB
if (rewritten) {
if (!done) {
// See if we need to replace the query string
- String urlStringDecoded = urlDecoded.toString();
- String queryStringDecoded = null;
- int queryIndex = urlStringDecoded.indexOf('?');
+ String urlStringRewriteEncoded = urlRewriteEncoded.toString();
+ String queryStringRewriteEncoded = null;
+ int queryIndex = urlStringRewriteEncoded.indexOf('?');
if (queryIndex != -1) {
- queryStringDecoded = urlStringDecoded.substring(queryIndex+1);
- urlStringDecoded = urlStringDecoded.substring(0, queryIndex);
+ queryStringRewriteEncoded = urlStringRewriteEncoded.substring(queryIndex + 1);
+ urlStringRewriteEncoded = urlStringRewriteEncoded.substring(0, queryIndex);
}
+ // Parse path parameters from rewrite production and populate request path parameters
+ urlStringRewriteEncoded = org.apache.catalina.util.RequestUtil.stripPathParams(urlStringRewriteEncoded, request);
// Save the current context path before re-writing starts
String contextPath = null;
if (context) {
@@ -499,26 +550,27 @@ public class RewriteValve extends ValveB
// This is neither decoded nor normalized
chunk.append(contextPath);
}
- chunk.append(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset));
+ // Step 3. Complete the 2nd stage to encoding.
+ chunk.append(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset));
request.getCoyoteRequest().requestURI().toChars();
// Decoded and normalized URI
// Rewriting may have denormalized the URL
- urlStringDecoded = RequestUtil.normalize(urlStringDecoded);
+ urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded);
request.getCoyoteRequest().decodedURI().setString(null);
chunk = request.getCoyoteRequest().decodedURI().getCharChunk();
chunk.recycle();
if (context) {
// This is decoded and normalized
chunk.append(request.getServletContext().getContextPath());
- }
- chunk.append(urlStringDecoded);
+ }
+ chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name()));
request.getCoyoteRequest().decodedURI().toChars();
// Set the new Query if there is one
- if (queryStringDecoded != null) {
+ if (queryStringRewriteEncoded != null) {
request.getCoyoteRequest().queryString().setString(null);
chunk = request.getCoyoteRequest().queryString().getCharChunk();
chunk.recycle();
- chunk.append(URLEncoder.QUERY.encode(queryStringDecoded, uriCharset));
+ chunk.append(REWRITE_QUERY_ENCODER.encode(queryStringRewriteEncoded, uriCharset));
if (qsa && originalQueryStringEncoded != null &&
originalQueryStringEncoded.length() > 0) {
chunk.append('&');
@@ -796,4 +848,31 @@ public class RewriteValve extends ValveB
throw new IllegalArgumentException(sm.getString("rewriteValve.invalidFlags", line, flag));
}
}
+
+
+ private CharSequence encodeForRewrite(CharSequence input) {
+ StringBuilder result = null;
+ int pos = 0;
+ int mark = 0;
+ while (pos < input.length()) {
+ char c = input.charAt(pos);
+ if (c == '%' || c == ';' || c == '?') {
+ if (result == null) {
+ result = new StringBuilder((int) (input.length() * 1.1));
+ }
+ result.append(input.subSequence(mark, pos));
+ result.append('%');
+ result.append(Character.forDigit((c >> 4) & 0xF, 16));
+ result.append(Character.forDigit(c & 0xF, 16));
+ mark = pos + 1;
+ }
+ pos++;
+ }
+ if (result != null) {
+ result.append(input.subSequence(mark, input.length()));
+ return result;
+ } else {
+ return input;
+ }
+ }
}
Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java
+++ apache-tomcat-9.0.36-src/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java
@@ -17,6 +17,7 @@
package org.apache.catalina.valves.rewrite;
import java.net.HttpURLConnection;
+import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
@@ -50,7 +51,7 @@ public class TestRewriteValve extends To
@Test
public void testBackslashPercentSign() throws Exception {
- doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%255A");
+ doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%5A");
}
@Test
@@ -119,6 +120,11 @@ public class TestRewriteValve extends To
}
@Test
+ public void testRewriteMap10() throws Exception {
+ doTestRewrite("RewriteMap lc int:escape\n" + "RewriteRule ^(.*) ${lc:$1}", "/c/a%20aa", "/c/a%20aa");
+ }
+
+ @Test
public void testRewriteServerVar() throws Exception {
doTestRewrite("RewriteRule /b/(.*).html$ /c%{SERVLET_PATH}", "/b/x.html", "/c/b/x.html");
}
@@ -227,7 +233,7 @@ public class TestRewriteValve extends To
public void testNonAsciiQueryStringWithB() throws Exception {
doTestRewrite("RewriteRule ^/b/(.*)/id=(.*) /c?filename=$1&id=$2 [B]",
"/b/file01/id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95", "/c",
- "filename=file01&id=%25E5%259C%25A8%25E7%25BA%25BF%25E6%25B5%258B%25E8%25AF%2595");
+ "filename=file01&id=%E5%9C%A8%E7%BA%BF%E6%B5%8B%E8%AF%95");
}
@@ -235,9 +241,8 @@ public class TestRewriteValve extends To
public void testNonAsciiQueryStringAndPathAndRedirectWithB() throws Exception {
// Note the double encoding of the result (httpd produces the same result)
doTestRewrite("RewriteRule ^/b/(.*)/(.*)/id=(.*) /c/$1?filename=$2&id=$3 [B,R]",
- "/b/%E5%9C%A8%E7%BA%BF/file01/id=%E6%B5%8B%E8%AF%95",
- "/c/%25E5%259C%25A8%25E7%25BA%25BF",
- "filename=file01&id=%25E6%25B5%258B%25E8%25AF%2595");
+ "/b/%E5%9C%A8%E7%BA%BF/file01/id=%E6%B5%8B%E8%AF%95", "/c/%E5%9C%A8%E7%BA%BF",
+ "filename=file01&id=%E6%B5%8B%E8%AF%95");
}
@@ -252,8 +257,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithBothQsFlagsB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]",
- "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE",
+ "/c/%C2%A1%C2%A1", "id=%C2%A1");
}
@@ -268,8 +273,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithBothQsFlagsRB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]",
- "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE",
+ "/c/%C2%A1%C2%A1", "id=%C2%A1");
}
@@ -296,9 +301,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithBothQsFlagsBQSA() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B,QSA]",
- "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/%C2%A1%25C2%25A1",
- "id=%25C2%25A1&di=%C2%AE");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE",
+ "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE");
}
@@ -314,9 +318,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithBothQsFlagsRBQSA() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,QSA]",
- "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/%C2%A1%25C2%25A1",
- "id=%25C2%25A1&di=%C2%AE");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,QSA]", "/b/%C2%A1/id=%C2%A1?di=%C2%AE",
+ "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE");
}
@@ -351,8 +354,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithOriginalQsFlagsB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]",
- "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%25C2%25A1", "id=%C2%A1");
+ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1",
+ "id=%C2%A1");
}
@@ -367,8 +370,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithOriginalQsFlagsRB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]",
- "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%25C2%25A1", "id=%C2%A1");
+ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1",
+ "id=%C2%A1");
}
@@ -403,8 +406,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithRewriteQsFlagsB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]",
- "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [B]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%C2%A1",
+ "id=%C2%A1");
}
@@ -428,8 +431,8 @@ public class TestRewriteValve extends To
@Test
public void testUtf8WithRewriteQsFlagsRB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]",
- "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%25C2%25A1", "id=%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B]", "/b/%C2%A1/id=%C2%A1", "/c/%C2%A1%C2%A1",
+ "id=%C2%A1");
}
@@ -472,7 +475,7 @@ public class TestRewriteValve extends To
@Test
public void testUtf8FlagsB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1", "/c/%C2%A1%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1", "/c/%C2%A1%C2%A1");
}
@@ -486,7 +489,7 @@ public class TestRewriteValve extends To
@Test
public void testUtf8FlagsRB() throws Exception {
// Note %C2%A1 == \u00A1
- doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1", "/c/%C2%A1%25C2%25A1");
+ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1", "/c/%C2%A1%C2%A1");
}
@@ -652,6 +655,7 @@ public class TestRewriteValve extends To
rewriteValve.setConfiguration(config);
Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+ ctx.addServletMappingDecoded("/a/Z", "snoop");
ctx.addServletMappingDecoded("/a/%5A", "snoop");
ctx.addServletMappingDecoded("/c/*", "snoop");
Tomcat.addServlet(ctx, "default", new DefaultServlet());
@@ -722,4 +726,87 @@ public class TestRewriteValve extends To
Assert.assertEquals(expectedStatusCode, rc);
}
}
+
+
+ @Test
+ public void testEncodedUriSimple() throws Exception {
+ doTestRewriteWithEncoding("aaa");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedQuestionMark01() throws Exception {
+ doTestRewriteWithEncoding("a%3fa");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedQuestionMark02() throws Exception {
+ doTestRewriteWithEncoding("%3faa");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedQuestionMark03() throws Exception {
+ doTestRewriteWithEncoding("aa%3f");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedQuestionMarkAndQueryString() throws Exception {
+ doTestRewriteWithEncoding("a%3fa?b=c", "a%3fa", "b=c");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedSemicolon01() throws Exception {
+ doTestRewriteWithEncoding("a%3ba");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedSemicolon02() throws Exception {
+ doTestRewriteWithEncoding("%3baa");
+ }
+
+
+ @Test
+ public void testEncodedUriEncodedSemicolon03() throws Exception {
+ doTestRewriteWithEncoding("aa%3b");
+ }
+
+
+ private void doTestRewriteWithEncoding(String segment) throws Exception {
+ doTestRewriteWithEncoding(segment, segment, null);
+ }
+
+ private void doTestRewriteWithEncoding(String segment, String expectedSegment, String expectedQueryString)
+ throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+
+ // No file system docBase required
+ Context ctx = tomcat.addContext("", null);
+
+ RewriteValve rewriteValve = new RewriteValve();
+ tomcat.getHost().getPipeline().addValve(rewriteValve);
+
+ rewriteValve.setConfiguration("RewriteRule ^/source/(.*)$ /target/$1");
+
+ Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+ ctx.addServletMappingDecoded("/target/*", "snoop");
+
+ tomcat.start();
+
+ ByteChunk res = new ByteChunk();
+ int rc = getUrl("http://localhost:" + getPort() + "/source/" + segment, res, false);
+
+ Assert.assertEquals(HttpServletResponse.SC_OK, rc);
+
+ res.setCharset(StandardCharsets.UTF_8);
+ String body = res.toString();
+ Assert.assertTrue(body, body.contains("REQUEST-URI: /target/" + expectedSegment));
+ Assert.assertTrue(body, body.contains("PATH-INFO: /" +
+ URLDecoder.decode(expectedSegment, StandardCharsets.UTF_8)));
+ Assert.assertTrue(body, body.contains("REQUEST-QUERY-STRING: " + expectedQueryString));
+ }
}
Index: apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
===================================================================
--- apache-tomcat-9.0.36-src.orig/webapps/docs/changelog.xml
+++ apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
@@ -134,6 +134,11 @@
explicitly set the HTTP response status to 500 as the
<code>ServerAuthContext</code> may not have set it. (markt)
</fix>
+ <fix>
+ Improve the handling of <code>%nn</code> URL encoding in the
+ RewriteValve and document how <code>%nn</code> URL encoding may be used
+ with rewrite rules. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/util/RequestUtil.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/util/RequestUtil.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/util/RequestUtil.java
@@ -18,6 +18,8 @@ package org.apache.catalina.util;
import javax.servlet.http.HttpServletRequest;
+import org.apache.catalina.connector.Request;
+
/**
* General purpose request parsing and encoding utility methods.
*
@@ -57,4 +59,51 @@ public final class RequestUtil {
return url;
}
+
+
+ /**
+ * Strip parameters for given path.
+ *
+ * @param input the input path
+ * @param request the request to add the parameters to
+ *
+ * @return the cleaned path
+ */
+ public static String stripPathParams(String input, Request request) {
+ // Shortcut
+ if (input.indexOf(';') < 0) {
+ return input;
+ }
+
+ StringBuilder sb = new StringBuilder(input.length());
+ int pos = 0;
+ int limit = input.length();
+ while (pos < limit) {
+ int nextSemiColon = input.indexOf(';', pos);
+ if (nextSemiColon < 0) {
+ nextSemiColon = limit;
+ }
+ sb.append(input, pos, nextSemiColon);
+ int followingSlash = input.indexOf('/', nextSemiColon);
+ if (followingSlash < 0) {
+ pos = limit;
+ } else {
+ pos = followingSlash;
+ }
+ if (request != null && nextSemiColon + 1 < pos) {
+ String pathVariablesString = input.substring(nextSemiColon + 1, pos);
+ String[] pathVariables = pathVariablesString.split(";");
+ for (String pathVariable : pathVariables) {
+ int equals = pathVariable.indexOf('=');
+ if (equals > -1 && equals + 1 < pathVariable.length()) {
+ String name = pathVariable.substring(0, equals);
+ String value = pathVariable.substring(equals + 1);
+ request.addPathParameter(name, value);
+ }
+ }
+ }
+ }
+
+ return sb.toString();
+ }
}
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/connector/Request.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/connector/Request.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/connector/Request.java
@@ -441,11 +441,11 @@ public class Request implements HttpServ
// --------------------------------------------------------- Public Methods
- protected void addPathParameter(String name, String value) {
+ public void addPathParameter(String name, String value) {
coyoteRequest.addPathParameter(name, value);
}
- protected String getPathParameter(String name) {
+ public String getPathParameter(String name) {
return coyoteRequest.getPathParameter(name);
}