File tomcat-9.0.36-CVE-2025-55752.patch of Package tomcat.43093
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
@@ -327,7 +327,7 @@ public class RewriteValve extends ValveB
// As long as MB isn't a char sequence or affiliated, this has to be
// converted to a string
Charset uriCharset = request.getConnector().getURICharset();
- String originalQueryStringEncoded = request.getQueryString();
+ String queryStringOriginalEncoded = request.getQueryString();
MessageBytes urlMB =
context ? request.getRequestPathMB() : request.getDecodedRequestURIMB();
urlMB.toChars();
@@ -424,11 +424,10 @@ public class RewriteValve extends ValveB
StringBuilder urlStringEncoded =
new StringBuilder(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset));
- if (!qsd && originalQueryStringEncoded != null
- && originalQueryStringEncoded.length() > 0) {
+ if (!qsd && queryStringOriginalEncoded != null && !queryStringOriginalEncoded.isEmpty()) {
if (rewrittenQueryStringRewriteEncoded == null) {
urlStringEncoded.append('?');
- urlStringEncoded.append(originalQueryStringEncoded);
+ urlStringEncoded.append(queryStringOriginalEncoded);
} else {
if (qsa) {
// if qsa is specified append the query
@@ -436,7 +435,7 @@ public class RewriteValve extends ValveB
urlStringEncoded.append(
REWRITE_QUERY_ENCODER.encode(rewrittenQueryStringRewriteEncoded, uriCharset));
urlStringEncoded.append('&');
- urlStringEncoded.append(originalQueryStringEncoded);
+ urlStringEncoded.append(queryStringOriginalEncoded);
} else if (index == urlStringEncoded.length() - 1) {
// if the ? is the last character delete it, its only purpose was to
// prevent the rewrite module from appending the query string
@@ -553,9 +552,10 @@ public class RewriteValve extends ValveB
// 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
- urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded);
+ // Rewriting may have denormalized the URL and added encoded characters
+ // Decode then normalize
+ String urlStringRewriteDecoded = URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name());
+ urlStringRewriteDecoded = RequestUtil.normalize(urlStringRewriteDecoded);
request.getCoyoteRequest().decodedURI().setString(null);
chunk = request.getCoyoteRequest().decodedURI().getCharChunk();
chunk.recycle();
@@ -563,18 +563,23 @@ public class RewriteValve extends ValveB
// This is decoded and normalized
chunk.append(request.getServletContext().getContextPath());
}
- chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name()));
+ chunk.append(urlStringRewriteDecoded);
request.getCoyoteRequest().decodedURI().toChars();
- // Set the new Query if there is one
- if (queryStringRewriteEncoded != null) {
+ // Set the new Query String
+ if (queryStringRewriteEncoded == null) {
+ // No new query string. Therefore the original is retained unless QSD is defined.
+ if (qsd) {
+ request.getCoyoteRequest().queryString().setString(null);
+ }
+ } else {
+ // New query string. Therefore the original is dropped unless QSA is defined (and QSD is not).
request.getCoyoteRequest().queryString().setString(null);
chunk = request.getCoyoteRequest().queryString().getCharChunk();
chunk.recycle();
chunk.append(REWRITE_QUERY_ENCODER.encode(queryStringRewriteEncoded, uriCharset));
- if (qsa && originalQueryStringEncoded != null &&
- originalQueryStringEncoded.length() > 0) {
+ if (qsa && queryStringOriginalEncoded != null && !queryStringOriginalEncoded.isEmpty()) {
chunk.append('&');
- chunk.append(originalQueryStringEncoded);
+ chunk.append(queryStringOriginalEncoded);
}
if (!chunk.isNull()) {
request.getCoyoteRequest().queryString().toChars();
@@ -665,6 +670,10 @@ public class RewriteValve extends ValveB
while (flagsTokenizer.hasMoreElements()) {
parseRuleFlag(line, rule, flagsTokenizer.nextToken());
}
+ // If QSD and QSA are present, QSD always takes precedence
+ if (rule.isQsdiscard()) {
+ rule.setQsappend(false);
+ }
}
return rule;
} else if (token.equals("RewriteMap")) {
Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/startup/TomcatBaseTest.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/test/org/apache/catalina/startup/TomcatBaseTest.java
+++ apache-tomcat-9.0.36-src/test/org/apache/catalina/startup/TomcatBaseTest.java
@@ -138,6 +138,14 @@ public abstract class TomcatBaseTest ext
return tomcat;
}
+ public Context getProgrammaticRootContext() {
+ // No file system docBase required
+ Context ctx = tomcat.addContext("", null);
+ // Disable class path scanning - it slows the tests down by almost an order of magnitude
+ ((StandardJarScanner) ctx.getJarScanner()).setScanClassPath(false);
+ return ctx;
+ }
+
/*
* Sub-classes need to know port so they can connect
*/
@@ -551,7 +559,7 @@ public abstract class TomcatBaseTest ext
value.append(";");
}
}
- out.println("PARAM/" + name + ": " + value);
+ out.println("PARAM:" + name + ": " + value);
}
out.println("SESSION-REQUESTED-ID: " +
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
@@ -23,6 +23,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+
import org.junit.Assert;
import org.junit.Test;
@@ -183,17 +185,112 @@ public class TestRewriteValve extends To
}
@Test
- public void testQueryString() throws Exception {
+ public void testQueryStringTargetOnly() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?je=2", "/b/id=1", "/c/id=1", "je=2");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQSA() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?je=2 [QSA]", "/b/id=1", "/c/id=1", "je=2");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?je=2 [QSD]", "/b/id=1", "/c/id=1", "je=2");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQSAQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?je=2 [QSA,QSD]", "/b/id=1", "/c/id=1", "je=2");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQS() throws Exception {
doTestRewrite("RewriteRule ^/b/(.*) /c?$1", "/b/id=1", "/c", "id=1");
}
@Test
+ public void testQueryStringTargetOnlyQSAQS() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c?$1 [QSA]", "/b/id=1", "/c", "id=1");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQSDQS() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c?$1 [QSD]", "/b/id=1", "/c", "id=1");
+ }
+
+ @Test
+ public void testQueryStringTargetOnlyQSAQSDQS() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c?$1 [QSA,QSD]", "/b/id=1", "/c", "id=1");
+ }
+
+ @Test
+ public void testQueryStringSourceOnly() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1", "/b/d?id=1", "/c/d", "id=1");
+ }
+
+ @Test
+ public void testQueryStringSourceOnlyQSA() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSA]", "/b/d?id=1", "/c/d", "id=1");
+ }
+
+ @Test
+ public void testQueryStringSourceOnlyQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSD]", "/b/d?id=1", "/c/d", null);
+ }
+
+ @Test
+ public void testQueryStringSourceOnlyQSAQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSA,QSD]", "/b/d?id=1", "/c/d", null);
+ }
+
+ @Test
+ public void testQueryStringSourceAndTarget() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?id=1", "/b/d?je=2", "/c/d", "id=1");
+ }
+
+ @Test
+ public void testQueryStringSourceAndTargetQSA() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?id=1 [QSA]", "/b/d?je=2", "/c/d", "id=1&je=2");
+ }
+
+ @Test
+ public void testQueryStringSourceAndTargetQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?id=1 [QSD]", "/b/d?je=2", "/c/d", "id=1");
+ }
+
+ @Test
+ public void testQueryStringSourceAndTargetQSAQSD() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?id=1 [QSA,QSD]", "/b/d?je=2", "/c/d", "id=1");
+ }
+
+ @Test
+ public void testQueryStringEncoded01() throws Exception {
+ doTestRewrite("RewriteCond %{QUERY_STRING} a=(.*)\nRewriteRule ^/b.*$ /%1 [QSD]", "/b?a=c", "/c", null);
+ }
+
+ @Test
+ public void testQueryStringEncoded02() throws Exception {
+ doTestRewrite("RewriteCond %{QUERY_STRING} a=(.*)\nRewriteRule ^/b.*$ /z/%1 [QSD]", "/b?a=%2e%2e%2fc%2faAbB", "/z/%2e%2e%2fc%2faAbB", null);
+ }
+
+ @Test
public void testQueryStringRemove() throws Exception {
- doTestRewrite("RewriteRule ^/b/(.*) /c/$1?", "/b/d?=1", "/c/d", null);
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?", "/b/d?id=1", "/c/d", null);
}
@Test
public void testQueryStringRemove02() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSD]", "/b/d?id=1", "/c/d", null);
+ }
+
+ @Test
+ public void testQueryStringRemoveInvalid() throws Exception {
+ doTestRewrite("RewriteRule ^/b/(.*) /c/$1?", "/b/d?=1", "/c/d", null);
+ }
+
+ @Test
+ public void testQueryStringRemoveInvalid02() throws Exception {
doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSD]", "/b/d?=1", "/c/d", null);
}
@@ -514,9 +611,8 @@ public class TestRewriteValve extends To
@Test
public void testFlagsNC() throws Exception {
// https://bz.apache.org/bugzilla/show_bug.cgi?id=60116
- doTestRewrite("RewriteCond %{QUERY_STRING} a=([a-z]*) [NC]\n"
- + "RewriteRule .* - [E=X-Test:%1]",
- "/c?a=aAa", "/c", null, "aAa");
+ doTestRewrite("RewriteCond %{QUERY_STRING} a=([a-z]*) [NC]\n" + "RewriteRule .* - [E=X-Test:%1]", "/c?a=aAa",
+ "/c", "a=aAa", "aAa");
}
@@ -672,12 +768,16 @@ public class TestRewriteValve extends To
// were written into the request target
Assert.assertEquals(400, rc);
} else {
+ // If there is an expected URI, the request should be successful
+ Assert.assertEquals(200, rc);
String body = res.toString();
RequestDescriptor requestDesc = SnoopResult.parse(body);
String requestURI = requestDesc.getRequestInfo("REQUEST-URI");
Assert.assertEquals(expectedURI, requestURI);
- if (expectedQueryString != null) {
+ if (expectedQueryString == null) {
+ Assert.assertTrue(requestDesc.getParams().isEmpty());
+ } else {
String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING");
Assert.assertEquals(expectedQueryString, queryString);
}