diff --git a/CVE-2025-31650-1.patch b/CVE-2025-31650-1.patch new file mode 100644 index 0000000..d7934cd --- /dev/null +++ b/CVE-2025-31650-1.patch @@ -0,0 +1,110 @@ +From 40ae788c2e64d018b4e58cd4210bb96434d0100d Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 18 Mar 2025 12:24:09 +0000 +Subject: [PATCH] Fix BZ 69614 - invalid priority field values should be + ignored + +Origin: https://github.com/apache/tomcat/commit/40ae788c2e64d018b4e58cd4210bb96434d0100d + +--- + java/org/apache/coyote/http2/Http2Parser.java | 23 +++++++++++------ + .../coyote/http2/LocalStrings.properties | 1 + + test/org/apache/coyote/http2/TestRfc9218.java | 25 +++++++++++++++++++ + 4 files changed, 46 insertions(+), 7 deletions(-) + +diff --git a/java/org/apache/coyote/http2/Http2Parser.java b/java/org/apache/coyote/http2/Http2Parser.java +index db7c2fd496a2..90c1142616b7 100644 +--- a/java/org/apache/coyote/http2/Http2Parser.java ++++ b/java/org/apache/coyote/http2/Http2Parser.java +@@ -477,15 +477,24 @@ protected void readPriorityUpdateFrame(int payloadSize, ByteBuffer buffer) throw + + ByteArrayInputStream bais = new ByteArrayInputStream(payload, 4, payloadSize - 4); + Reader r = new BufferedReader(new InputStreamReader(bais, StandardCharsets.US_ASCII)); +- Priority p = Priority.parsePriority(r); + +- if (log.isTraceEnabled()) { +- log.trace(sm.getString("http2Parser.processFramePriorityUpdate.debug", connectionId, +- Integer.toString(prioritizedStreamID), Integer.toString(p.getUrgency()), +- Boolean.valueOf(p.getIncremental()))); +- } ++ try { ++ Priority p = Priority.parsePriority(r); + +- output.priorityUpdate(prioritizedStreamID, p); ++ if (log.isTraceEnabled()) { ++ log.trace(sm.getString("http2Parser.processFramePriorityUpdate.debug", connectionId, ++ Integer.toString(prioritizedStreamID), Integer.toString(p.getUrgency()), ++ Boolean.valueOf(p.getIncremental()))); ++ } ++ ++ output.priorityUpdate(prioritizedStreamID, p); ++ } catch (IllegalArgumentException iae) { ++ // Priority frames with invalid priority field values should be ignored ++ if (log.isTraceEnabled()) { ++ log.trace(sm.getString("http2Parser.processFramePriorityUpdate.invalid", connectionId, ++ Integer.toString(prioritizedStreamID)), iae); ++ } ++ } + } + + +diff --git a/java/org/apache/coyote/http2/LocalStrings.properties b/java/org/apache/coyote/http2/LocalStrings.properties +index 6ab82e8bdb9b..114f546017d4 100644 +--- a/java/org/apache/coyote/http2/LocalStrings.properties ++++ b/java/org/apache/coyote/http2/LocalStrings.properties +@@ -77,6 +77,7 @@ http2Parser.processFrameHeaders.decodingDataLeft=Data left over after HPACK deco + http2Parser.processFrameHeaders.decodingFailed=There was an error during the HPACK decoding of HTTP headers + http2Parser.processFrameHeaders.payload=Connection [{0}], Stream [{1}], Processing headers payload of size [{2}] + http2Parser.processFramePriorityUpdate.debug=Connection [{0}], Stream [{1}], Urgency [{2}], Incremental [{3}] ++http2Parser.processFramePriorityUpdate.invalid=Connection [{0}], Stream [{1}], Priority Update frame with invalid priority field value + http2Parser.processFramePriorityUpdate.streamZero=Connection [{0}], Priority update frame received to prioritize stream zero + http2Parser.processFramePushPromise=Connection [{0}], Stream [{1}], Push promise frames should not be sent by the client + http2Parser.processFrameSettings.ackWithNonZeroPayload=Settings frame received with the ACK flag set and payload present +diff --git a/test/org/apache/coyote/http2/TestRfc9218.java b/test/org/apache/coyote/http2/TestRfc9218.java +index eb9256d2a140..1a6081f88c99 100644 +--- a/test/org/apache/coyote/http2/TestRfc9218.java ++++ b/test/org/apache/coyote/http2/TestRfc9218.java +@@ -17,6 +17,7 @@ + package org.apache.coyote.http2; + + import java.io.IOException; ++import java.nio.charset.StandardCharsets; + + import org.junit.Assert; + import org.junit.Test; +@@ -146,6 +147,9 @@ public void testPriority() throws Exception { + // 19 - 7021 body left + // 21 - 6143 body left + ++ // BZ 69614 - invalid priority update frames should be ignored ++ sendInvalidPriorityUpdate(17); ++ + // Re-order the priorities + sendPriorityUpdate(17, 2, true); + +@@ -191,4 +195,25 @@ public void testPriority() throws Exception { + ioe.printStackTrace(); + } + } ++ ++ ++ private void sendInvalidPriorityUpdate(int streamId) throws IOException { ++ byte[] payload = "u=1:i".getBytes(StandardCharsets.US_ASCII); ++ ++ byte[] priorityUpdateFrame = new byte[13 + payload.length]; ++ ++ // length ++ ByteUtil.setThreeBytes(priorityUpdateFrame, 0, 4 + payload.length); ++ // type ++ priorityUpdateFrame[3] = FrameType.PRIORITY_UPDATE.getIdByte(); ++ // Stream ID ++ ByteUtil.set31Bits(priorityUpdateFrame, 5, 0); ++ ++ // Payload ++ ByteUtil.set31Bits(priorityUpdateFrame, 9, streamId); ++ System.arraycopy(payload, 0, priorityUpdateFrame, 13, payload.length); ++ ++ os.write(priorityUpdateFrame); ++ os.flush(); ++ } + } diff --git a/CVE-2025-31650-2.patch b/CVE-2025-31650-2.patch new file mode 100644 index 0000000..467aa51 --- /dev/null +++ b/CVE-2025-31650-2.patch @@ -0,0 +1,28 @@ +From b98e74f517b36929f4208506e5adad22cb767baa Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Wed, 19 Mar 2025 03:21:50 +0000 +Subject: [PATCH] Additional fix for BZ 69614 + +Origin: https://github.com/apache/tomcat/commit/b98e74f517b36929f4208506e5adad22cb767baa + +--- + java/org/apache/coyote/http2/Stream.java | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java +index e8f0d9481c03..ecda8caec811 100644 +--- a/java/org/apache/coyote/http2/Stream.java ++++ b/java/org/apache/coyote/http2/Stream.java +@@ -456,6 +456,12 @@ public final void emitHeader(String name, String value) throws HpackException { + setIncremental(p.getIncremental()); + } catch (IOException ioe) { + // Not possible with StringReader ++ } catch (IllegalArgumentException iae) { ++ // Invalid priority header field values should be ignored ++ if (log.isTraceEnabled()) { ++ log.trace(sm.getString("http2Parser.processFramePriorityUpdate.invalid", getConnectionId(), ++ getIdAsString()), iae); ++ } + } + break; + } diff --git a/CVE-2025-31650-3.patch b/CVE-2025-31650-3.patch new file mode 100644 index 0000000..75a21b6 --- /dev/null +++ b/CVE-2025-31650-3.patch @@ -0,0 +1,51 @@ +From b7674782679e1514a0d154166b1d04d38aaac4a9 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Wed, 19 Mar 2025 03:47:11 +0000 +Subject: [PATCH] Improve handling of unexpected errors during HTTP/2 + processing + +Origin: https://github.com/apache/tomcat/commit/b7674782679e1514a0d154166b1d04d38aaac4a9 + +--- + java/org/apache/coyote/http2/Http2UpgradeHandler.java | 8 ++++++++ + java/org/apache/coyote/http2/LocalStrings.properties | 1 + + 3 files changed, 12 insertions(+) + +diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java b/java/org/apache/coyote/http2/Http2UpgradeHandler.java +index 54e119b3f36b..38a81309a6e0 100644 +--- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java ++++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java +@@ -47,6 +47,7 @@ + import org.apache.coyote.http2.Http2Parser.Output; + import org.apache.juli.logging.Log; + import org.apache.juli.logging.LogFactory; ++import org.apache.tomcat.util.ExceptionUtils; + import org.apache.tomcat.util.http.MimeHeaders; + import org.apache.tomcat.util.http.parser.Priority; + import org.apache.tomcat.util.log.UserDataHelper; +@@ -439,6 +440,13 @@ public SocketState upgradeDispatch(SocketEvent status) { + log.debug(sm.getString("upgradeHandler.ioerror", connectionId), ioe); + } + close(); ++ } catch (Throwable t) { ++ ExceptionUtils.handleThrowable(t); ++ if (log.isDebugEnabled()) { ++ log.debug(sm.getString("upgradeHandler.throwable", connectionId), t); ++ } ++ // Unexpected errors close the connection. ++ close(); + } + + if (log.isTraceEnabled()) { +diff --git a/java/org/apache/coyote/http2/LocalStrings.properties b/java/org/apache/coyote/http2/LocalStrings.properties +index 114f546017d4..61cb8497bf4b 100644 +--- a/java/org/apache/coyote/http2/LocalStrings.properties ++++ b/java/org/apache/coyote/http2/LocalStrings.properties +@@ -160,6 +160,7 @@ upgradeHandler.stream.error=Connection [{0}], Stream [{1}] Closed due to error + upgradeHandler.stream.even=A new remote stream ID of [{0}] was requested but all remote streams must use odd identifiers + upgradeHandler.stream.notWritable=Connection [{0}], Stream [{1}], This stream is in state [{2}] and is not writable + upgradeHandler.stream.old=A new remote stream ID of [{0}] was requested but the most recent stream was [{1}] ++upgradeHandler.throwable=Connection [{0}] + upgradeHandler.tooManyRemoteStreams=The client attempted to use more than [{0}] active streams + upgradeHandler.tooMuchOverhead=Connection [{0}], Too much overhead so the connection will be closed + upgradeHandler.unexpectedAck=Connection [{0}], Stream [{1}], A settings acknowledgement was received when not expected diff --git a/CVE-2025-31651-1.patch b/CVE-2025-31651-1.patch new file mode 100644 index 0000000..820c905 --- /dev/null +++ b/CVE-2025-31651-1.patch @@ -0,0 +1,562 @@ +From ee3ab548e92345eca0cbd1f01649eb36c6f29454 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Thu, 13 Mar 2025 15:06:26 +0000 +Subject: [PATCH] Better handling of URLs with literal ';' and '?' + +Origin: https://github.com/apache/tomcat/commit/ee3ab548e92345eca0cbd1f01649eb36c6f29454 + +--- + .../catalina/connector/CoyoteAdapter.java | 18 +-- + .../catalina/valves/rewrite/RewriteValve.java | 137 ++++++++++++++---- + .../valves/rewrite/TestRewriteValve.java | 119 ++++++++++++--- + webapps/docs/rewrite.xml | 22 +++ + 4 files changed, 241 insertions(+), 55 deletions(-) + +diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java +index 3c09f24..00086bd 100644 +--- a/java/org/apache/catalina/connector/CoyoteAdapter.java ++++ b/java/org/apache/catalina/connector/CoyoteAdapter.java +@@ -653,17 +653,17 @@ public class CoyoteAdapter implements Adapter { + } 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() ++ * 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); +- } + } + } + +diff --git a/java/org/apache/catalina/valves/rewrite/RewriteValve.java b/java/org/apache/catalina/valves/rewrite/RewriteValve.java +index 6eb01e5..d1071e9 100644 +--- a/java/org/apache/catalina/valves/rewrite/RewriteValve.java ++++ b/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; +@@ -64,6 +65,24 @@ import org.apache.tomcat.util.http.RequestUtil; + */ + 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. + */ +@@ -297,22 +316,51 @@ public class RewriteValve extends ValveBase { + + invoked.set(Boolean.TRUE); + +- // As long as MB isn't a char sequence or affiliated, this has to be +- // converted to a string ++ // 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(); + MessageBytes urlMB = 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. ++ * ++ * The re-write rules cannot insert path parameters. ++ * ++ * 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; + boolean valveSkip = 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.toString().equals(newtest.toString())) { + if (containerLog.isTraceEnabled()) { +@@ -322,7 +370,7 @@ public class RewriteValve extends ValveBase { + if (rule.isHost()) { + host = newtest; + } else { +- urlDecoded = newtest; ++ urlRewriteEncoded = newtest; + } + rewritten = true; + } +@@ -359,28 +407,29 @@ public class RewriteValve extends ValveBase { + 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); + } + +- StringBuilder urlStringEncoded = +- new StringBuilder(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) { +@@ -389,13 +438,14 @@ public class RewriteValve extends ValveBase { + 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 +@@ -470,12 +520,12 @@ public class RewriteValve extends ValveBase { + 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); + } + // Save the current context path before re-writing starts + String contextPath = null; +@@ -489,22 +539,24 @@ public class RewriteValve extends ValveBase { + // 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)); + // Decoded and normalized URI + // Rewriting may have denormalized the URL +- urlStringDecoded = RequestUtil.normalize(urlStringDecoded); ++ urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded); + request.getCoyoteRequest().decodedURI().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().decodedURI().getCharChunk(); + if (context) { + // This is decoded and normalized + chunk.append(request.getServletContext().getContextPath()); + } +- chunk.append(urlStringDecoded); ++ chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset)); + // Set the new Query if there is one +- if (queryStringDecoded != null) { ++ if (queryStringRewriteEncoded != null) { + request.getCoyoteRequest().queryString().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().queryString().getCharChunk(); +- chunk.append(URLEncoder.QUERY.encode(queryStringDecoded, uriCharset)); ++ chunk.append(REWRITE_QUERY_ENCODER.encode(queryStringRewriteEncoded, uriCharset)); + if (qsa && originalQueryStringEncoded != null && originalQueryStringEncoded.length() > 0) { + chunk.append('&'); + chunk.append(originalQueryStringEncoded); +@@ -799,4 +851,31 @@ public class RewriteValve extends ValveBase { + 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; ++ } ++ } + } +diff --git a/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java b/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java +index 3a08886..5c737ae 100644 +--- a/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java ++++ b/test/org/apache/catalina/valves/rewrite/TestRewriteValve.java +@@ -20,6 +20,7 @@ import java.io.File; + import java.io.IOException; + import java.io.PrintWriter; + import java.net.HttpURLConnection; ++import java.net.URLDecoder; + import java.nio.charset.StandardCharsets; + import java.util.Arrays; + import java.util.HashMap; +@@ -63,7 +64,7 @@ public class TestRewriteValve extends TomcatBaseTest { + + @Test + public void testBackslashPercentSign() throws Exception { +- doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%255A"); ++ doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%5A"); + } + + @Test +@@ -142,7 +143,7 @@ public class TestRewriteValve extends TomcatBaseTest { + + @Test + public void testRewriteMap10() throws Exception { +- doTestRewrite("RewriteMap lc int:escape\n" + "RewriteRule ^(.*) ${lc:$1}", "/c/a%20aa", "/c/a%2520aa"); ++ doTestRewrite("RewriteMap lc int:escape\n" + "RewriteRule ^(.*) ${lc:$1}", "/c/a%20aa", "/c/a%20aa"); + } + + @Test +@@ -346,7 +347,7 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); + } + + +@@ -354,8 +355,8 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); + } + + +@@ -371,7 +372,7 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); ++ "/c/%C2%A1%C2%A1", "id=%C2%A1"); + } + + +@@ -387,7 +388,7 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); ++ "/c/%C2%A1%C2%A1", "id=%C2%A1"); + } + + +@@ -413,7 +414,7 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); ++ "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE"); + } + + +@@ -429,7 +430,7 @@ public class TestRewriteValve extends TomcatBaseTest { + 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"); ++ "/c/%C2%A1%C2%A1", "id=%C2%A1&di=%C2%AE"); + } + + +@@ -461,7 +462,7 @@ public class TestRewriteValve extends TomcatBaseTest { + @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", ++ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + +@@ -476,7 +477,7 @@ public class TestRewriteValve extends TomcatBaseTest { + @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", ++ doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B]", "/b/%C2%A1?id=%C2%A1", "/c/%C2%A1%C2%A1", + "id=%C2%A1"); + } + +@@ -510,8 +511,8 @@ public class TestRewriteValve extends TomcatBaseTest { + @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"); + } + + +@@ -534,8 +535,8 @@ public class TestRewriteValve extends TomcatBaseTest { + @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"); + } + + +@@ -575,7 +576,7 @@ public class TestRewriteValve extends TomcatBaseTest { + @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"); + } + + +@@ -589,7 +590,7 @@ public class TestRewriteValve extends TomcatBaseTest { + @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"); + } + + +@@ -784,6 +785,7 @@ public class TestRewriteValve extends TomcatBaseTest { + rewriteValve.setConfiguration(config); + + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); ++ ctx.addServletMappingDecoded("/a/Z", "snoop"); + ctx.addServletMappingDecoded("/a/%5A", "snoop"); + ctx.addServletMappingDecoded("/c/*", "snoop"); + ctx.addServletMappingDecoded("/W/*", "snoop"); +@@ -929,4 +931,87 @@ public class TestRewriteValve extends TomcatBaseTest { + } + } + } ++ ++ ++ @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)); ++ } + } +diff --git a/webapps/docs/rewrite.xml b/webapps/docs/rewrite.xml +index f153f9d..123d215 100644 +--- a/webapps/docs/rewrite.xml ++++ b/webapps/docs/rewrite.xml +@@ -56,6 +56,28 @@ + + + ++
++ ++

The URL presented to the rewrite valve is the same URL used for request ++ mapping with any literal '%', ';' and/or ++ '?' characters encoded in %nn form.

++ ++

A rewrite rule that wishes to insert a literal '%', ++ ';', '?', '&' or '=' ++ character should do so in %nn form. Other characters maybe ++ inserted in either literal or %nn form.

++ ++

This enables the rewrite rules to: ++

    ++
  • process URLs containing literal '?' characters;
  • ++
  • add a query string;
  • ++
  • insert a literal '%' character without it being confused with ++ %nn encoding.
  • ++
++

++ ++
++ +
+ +

The rewrite.config file contains a list of directives which closely +-- +2.48.1 + diff --git a/CVE-2025-31651-2.patch b/CVE-2025-31651-2.patch new file mode 100644 index 0000000..52f0028 --- /dev/null +++ b/CVE-2025-31651-2.patch @@ -0,0 +1,24 @@ +From 175dc75fc428930034a6c93fb52f830d955d8e64 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Mon, 31 Mar 2025 17:34:05 +0100 +Subject: [PATCH] Fix back-port + +Origin: https://github.com/apache/tomcat/commit/175dc75fc428930034a6c93fb52f830d955d8e64 + +--- + java/org/apache/catalina/valves/rewrite/RewriteValve.java | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/java/org/apache/catalina/valves/rewrite/RewriteValve.java b/java/org/apache/catalina/valves/rewrite/RewriteValve.java +index f049e5eb7e40..b4e73f6845b4 100644 +--- a/java/org/apache/catalina/valves/rewrite/RewriteValve.java ++++ b/java/org/apache/catalina/valves/rewrite/RewriteValve.java +@@ -552,7 +552,7 @@ public void invoke(Request request, Response response) throws IOException, Servl + // This is decoded and normalized + chunk.append(request.getServletContext().getContextPath()); + } +- chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset)); ++ chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name())); + // Set the new Query if there is one + if (queryStringRewriteEncoded != null) { + request.getCoyoteRequest().queryString().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); diff --git a/tomcat.spec b/tomcat.spec index 77daf97..fe2cf55 100644 --- a/tomcat.spec +++ b/tomcat.spec @@ -23,7 +23,7 @@ Name: tomcat Epoch: 1 Version: %{major_version}.%{minor_version}.%{micro_version} -Release: 1 +Release: 2 Summary: Apache Servlet/JSP Engine, RI for Servlet %{servletspec}/JSP %{jspspec} API License: Apache-2.0 @@ -51,6 +51,11 @@ Patch4: rhbz-1857043.patch # remove bnd dependency which version is too low on rhel8 Patch6: remove-bnd-annotation.patch Patch7: build-with-jdk-1.8.patch +Patch8: CVE-2025-31650-1.patch +Patch9: CVE-2025-31650-2.patch +Patch10: CVE-2025-31650-3.patch +Patch11: CVE-2025-31651-1.patch +Patch12: CVE-2025-31651-2.patch BuildArch: noarch @@ -417,6 +422,9 @@ fi %{appdir}/docs %changelog +* Tue Apr 29 2025 wangkai <13474090681@163.com> - 1:9.0.100-2 +- Fix CVE-2025-31650, CVE-2025-31651 + * Fri Feb 21 2025 wangkai <13474090681@163.com> - 1:9.0.100-1 - Update to 9.0.100 for fix CVE-2024-56337 - Add a check to ensure that, if one or more web applications are potentially vulnerable to CVE-2024-56337,