diff --git a/SOURCES/message-byte-conversion.patch b/SOURCES/message-byte-conversion.patch new file mode 100644 index 0000000..3e32c21 --- /dev/null +++ b/SOURCES/message-byte-conversion.patch @@ -0,0 +1,483 @@ +--- java/org/apache/catalina/core/ApplicationContext.java ++++ java/org/apache/catalina/core/ApplicationContext.java +@@ -465,6 +465,7 @@ + + try { + // Map the URI ++ uriMB.setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + CharChunk uriCC = uriMB.getCharChunk(); + try { + uriCC.append(context.getPath()); +--- java/org/apache/catalina/mapper/Mapper.java ++++ java/org/apache/catalina/mapper/Mapper.java +@@ -695,6 +695,7 @@ + if (defaultHostName == null) { + return; + } ++ host.setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + host.getCharChunk().append(defaultHostName); + } + host.toChars(); +--- java/org/apache/catalina/valves/rewrite/RewriteValve.java ++++ java/org/apache/catalina/valves/rewrite/RewriteValve.java +@@ -492,49 +492,39 @@ + contextPath = request.getContextPath(); + } + // Populated the encoded (i.e. undecoded) requestURI +- request.getCoyoteRequest().requestURI().setString(null); ++ request.getCoyoteRequest().requestURI().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + CharChunk chunk = request.getCoyoteRequest().requestURI().getCharChunk(); +- chunk.recycle(); + if (context) { + // This is neither decoded nor normalized + chunk.append(contextPath); + } + chunk.append(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset)); +- request.getCoyoteRequest().requestURI().toChars(); + // Decoded and normalized URI + // Rewriting may have denormalized the URL + urlStringDecoded = RequestUtil.normalize(urlStringDecoded); +- request.getCoyoteRequest().decodedURI().setString(null); ++ request.getCoyoteRequest().decodedURI().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().decodedURI().getCharChunk(); +- chunk.recycle(); + if (context) { + // This is decoded and normalized + chunk.append(request.getServletContext().getContextPath()); + } + chunk.append(urlStringDecoded); +- request.getCoyoteRequest().decodedURI().toChars(); + // Set the new Query if there is one + if (queryStringDecoded != null) { +- request.getCoyoteRequest().queryString().setString(null); ++ request.getCoyoteRequest().queryString().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().queryString().getCharChunk(); +- chunk.recycle(); + chunk.append(URLEncoder.QUERY.encode(queryStringDecoded, uriCharset)); + if (qsa && originalQueryStringEncoded != null && + originalQueryStringEncoded.length() > 0) { + chunk.append('&'); + chunk.append(originalQueryStringEncoded); + } +- if (!chunk.isNull()) { +- request.getCoyoteRequest().queryString().toChars(); +- } + } + // Set the new host if it changed + if (!host.equals(request.getServerName())) { +- request.getCoyoteRequest().serverName().setString(null); ++ request.getCoyoteRequest().serverName().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().serverName().getCharChunk(); +- chunk.recycle(); + chunk.append(host.toString()); +- request.getCoyoteRequest().serverName().toChars(); + } + request.getMappingData().recycle(); + // Reinvoke the whole request recursively +--- java/org/apache/tomcat/util/buf/MessageBytes.java ++++ java/org/apache/tomcat/util/buf/MessageBytes.java +@@ -51,6 +51,8 @@ + was a char[] */ + public static final int T_CHARS = 3; + ++ public static final char[] EMPTY_CHAR_ARRAY = new char[0]; ++ + private int hashCode=0; + // did we compute the hashcode ? + private boolean hasHashCode=false; +@@ -61,9 +63,6 @@ + + // String + private String strValue; +- // true if a String value was computed. Probably not needed, +- // strValue!=null is the same +- private boolean hasStrValue=false; + + /** + * Creates a new, uninitialized MessageBytes object. +@@ -87,7 +86,7 @@ + } + + public boolean isNull() { +- return byteC.isNull() && charC.isNull() && !hasStrValue; ++ return type == T_NULL; + } + + /** +@@ -100,7 +99,6 @@ + + strValue=null; + +- hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } +@@ -116,7 +114,6 @@ + public void setBytes(byte[] b, int off, int len) { + byteC.setBytes( b, off, len ); + type=T_BYTES; +- hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } +@@ -131,7 +128,6 @@ + public void setChars( char[] c, int off, int len ) { + charC.setChars( c, off, len ); + type=T_CHARS; +- hasStrValue=false; + hasHashCode=false; + hasLongValue=false; + } +@@ -141,15 +137,13 @@ + * @param s The string + */ + public void setString( String s ) { +- strValue=s; +- hasHashCode=false; +- hasLongValue=false; ++ strValue = s; ++ hasHashCode = false; ++ hasLongValue = false; + if (s == null) { +- hasStrValue=false; +- type=T_NULL; ++ type = T_NULL; + } else { +- hasStrValue=true; +- type=T_STR; ++ type = T_STR; + } + } + +@@ -161,21 +155,22 @@ + */ + @Override + public String toString() { +- if (hasStrValue) { +- return strValue; +- } +- +- switch (type) { +- case T_CHARS: +- strValue = charC.toString(); +- hasStrValue = true; +- return strValue; +- case T_BYTES: +- strValue = byteC.toString(); +- hasStrValue = true; +- return strValue; +- } +- return null; ++ switch (type) { ++ case T_NULL: ++ case T_STR: ++ // No conversion required ++ break; ++ case T_BYTES: ++ type = T_STR; ++ strValue = byteC.toString(); ++ break; ++ case T_CHARS: ++ type = T_STR; ++ strValue = charC.toString(); ++ break; ++ } ++ ++ return strValue; + } + + //---------------------------------------- +@@ -232,21 +227,26 @@ + + + /** +- * Do a char->byte conversion. ++ * Convert to bytes and fill the ByteChunk with the converted value. + */ + public void toBytes() { +- if (isNull()) { +- return; +- } +- if (!byteC.isNull()) { +- type = T_BYTES; +- return; +- } +- toString(); +- type = T_BYTES; +- Charset charset = byteC.getCharset(); +- ByteBuffer result = charset.encode(strValue); +- byteC.setBytes(result.array(), result.arrayOffset(), result.limit()); ++ switch (type) { ++ case T_NULL: ++ byteC.recycle(); ++ //$FALL-THROUGH$ ++ case T_BYTES: ++ // No conversion required ++ return; ++ case T_CHARS: ++ toString(); ++ //$FALL-THROUGH$ ++ case T_STR: { ++ type = T_BYTES; ++ Charset charset = byteC.getCharset(); ++ ByteBuffer result = charset.encode(strValue); ++ byteC.setBytes(result.array(), result.arrayOffset(), result.limit()); ++ } ++ } + } + + +@@ -255,18 +255,22 @@ + * XXX Not optimized - it converts to String first. + */ + public void toChars() { +- if (isNull()) { +- return; +- } +- if (!charC.isNull()) { +- type = T_CHARS; +- return; +- } +- // inefficient +- toString(); +- type = T_CHARS; +- char cc[] = strValue.toCharArray(); +- charC.setChars(cc, 0, cc.length); ++ switch (type) { ++ case T_NULL: ++ charC.recycle(); ++ //$FALL-THROUGH$ ++ case T_CHARS: ++ // No conversion required ++ return; ++ case T_BYTES: ++ toString(); ++ //$FALL-THROUGH$ ++ case T_STR: { ++ type = T_CHARS; ++ char cc[] = strValue.toCharArray(); ++ charC.setChars(cc, 0, cc.length); ++ } ++ } + } + + +@@ -535,7 +539,6 @@ + end--; + } + longValue=l; +- hasStrValue=false; + hasHashCode=false; + hasLongValue=true; + type=T_BYTES; +--- ++++ test/org/apache/tomcat/util/buf/TestMessageBytesConversion.java +@@ -0,0 +1,207 @@ ++/* ++ * Licensed to the Apache Software Foundation (ASF) under one or more ++ * contributor license agreements. See the NOTICE file distributed with ++ * this work for additional information regarding copyright ownership. ++ * The ASF licenses this file to You under the Apache License, Version 2.0 ++ * (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++package org.apache.tomcat.util.buf; ++ ++import java.nio.charset.StandardCharsets; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.List; ++import java.util.function.Consumer; ++ ++import org.junit.Assert; ++import org.junit.Test; ++import org.junit.runner.RunWith; ++import org.junit.runners.Parameterized; ++import org.junit.runners.Parameterized.Parameter; ++import org.junit.runners.Parameterized.Parameters; ++ ++/** ++ * Checks that all MessageBytes getters are consistent with most recently used ++ * setter. ++ */ ++@RunWith(Parameterized.class) ++public class TestMessageBytesConversion { ++ ++ private static final String PREVIOUS_STRING = "previous-string"; ++ private static final byte[] PREVIOUS_BYTES = "previous-bytes".getBytes(StandardCharsets.ISO_8859_1); ++ private static final char[] PREVIOUS_CHARS = "previous-chars".toCharArray(); ++ ++ private static final String EXPECTED_STRING = "expected"; ++ private static final byte[] EXPECTED_BYTES = "expected".getBytes(StandardCharsets.ISO_8859_1); ++ private static final char[] EXPECTED_CHARS = "expected".toCharArray(); ++ ++ @Parameters(name = "{index}: previous({0}, {1}, {2}, {3}), set {4}, check({5}, {6}, {7}") ++ public static Collection parameters() { ++ List previousTypes = new ArrayList<>(); ++ previousTypes.add(MessageBytesType.BYTE); ++ previousTypes.add(MessageBytesType.CHAR); ++ previousTypes.add(MessageBytesType.STRING); ++ previousTypes.add(MessageBytesType.NULL); ++ ++ List setTypes = new ArrayList<>(); ++ setTypes.add(MessageBytesType.BYTE); ++ setTypes.add(MessageBytesType.CHAR); ++ setTypes.add(MessageBytesType.STRING); ++ ++ List parameterSets = new ArrayList<>(); ++ ++ List> previousPermutations = permutations(previousTypes); ++ List> checkPermutations = permutations(setTypes); ++ ++ for (List setPermutation : previousPermutations) { ++ for (MessageBytesType setType : setTypes) { ++ for (List checkPermutation : checkPermutations) { ++ parameterSets.add(new Object[] { ++ setPermutation.get(0), setPermutation.get(1), setPermutation.get(2), setPermutation.get(3), ++ setType, ++ checkPermutation.get(0), checkPermutation.get(1), checkPermutation.get(2)}); ++ } ++ } ++ } ++ ++ ++ return parameterSets; ++ } ++ ++ @Parameter(0) ++ public MessageBytesType setFirst; ++ @Parameter(1) ++ public MessageBytesType setSecond; ++ @Parameter(2) ++ public MessageBytesType setThird; ++ @Parameter(3) ++ public MessageBytesType setFourth; ++ ++ @Parameter(4) ++ public MessageBytesType expected; ++ ++ @Parameter(5) ++ public MessageBytesType checkFirst; ++ @Parameter(6) ++ public MessageBytesType checkSecond; ++ @Parameter(7) ++ public MessageBytesType checkThird; ++ ++ ++ @Test ++ public void testConversion() { ++ MessageBytes mb = MessageBytes.newInstance(); ++ ++ setFirst.setPrevious(mb); ++ setSecond.setPrevious(mb); ++ setThird.setPrevious(mb); ++ setFourth.setPrevious(mb); ++ ++ expected.setExpected(mb); ++ ++ checkFirst.checkExpected(mb); ++ checkSecond.checkExpected(mb); ++ checkThird.checkExpected(mb); ++ } ++ ++ ++ @Test ++ public void testConversionNull() { ++ MessageBytes mb = MessageBytes.newInstance(); ++ ++ setFirst.setPrevious(mb); ++ setSecond.setPrevious(mb); ++ setThird.setPrevious(mb); ++ setFourth.setPrevious(mb); ++ ++ mb.setString(null); ++ ++ checkFirst.checkNull(mb); ++ checkSecond.checkNull(mb); ++ checkThird.checkNull(mb); ++ } ++ ++ ++ public static enum MessageBytesType { ++ BYTE((x) -> x.setBytes(PREVIOUS_BYTES, 0, PREVIOUS_BYTES.length), ++ (x) -> x.setBytes(EXPECTED_BYTES, 0, EXPECTED_BYTES.length), ++ (x) -> {x.toBytes(); Assert.assertArrayEquals(EXPECTED_BYTES, x.getByteChunk().getBytes());}, ++ (x) -> {x.toBytes(); Assert.assertTrue(x.getByteChunk().isNull());}), ++ ++ CHAR((x) -> x.setChars(PREVIOUS_CHARS, 0, PREVIOUS_CHARS.length), ++ (x) -> x.setChars(EXPECTED_CHARS, 0, EXPECTED_CHARS.length), ++ (x) -> {x.toChars(); Assert.assertArrayEquals(EXPECTED_CHARS, x.getCharChunk().getChars());}, ++ (x) -> {x.toChars(); Assert.assertTrue(x.getCharChunk().isNull());}), ++ ++ STRING((x) -> x.setString(PREVIOUS_STRING), ++ (x) -> x.setString(EXPECTED_STRING), ++ (x) -> Assert.assertEquals(EXPECTED_STRING, x.toString()), ++ (x) -> Assert.assertNull(x.toString())), ++ ++ NULL((x) -> x.setString(null), ++ (x) -> x.setString(null), ++ (x) -> Assert.assertTrue(x.isNull()), ++ (x) -> Assert.assertTrue(x.isNull())); ++ ++ private final Consumer setPrevious; ++ private final Consumer setExpected; ++ private final Consumer checkExpected; ++ private final Consumer checkNull; ++ ++ private MessageBytesType(Consumer setPrevious, Consumer setExpected, ++ Consumer checkExpected, Consumer checkNull) { ++ this.setPrevious = setPrevious; ++ this.setExpected = setExpected; ++ this.checkExpected = checkExpected; ++ this.checkNull = checkNull; ++ } ++ ++ public void setPrevious(MessageBytes mb) { ++ setPrevious.accept(mb); ++ } ++ ++ public void setExpected(MessageBytes mb) { ++ setExpected.accept(mb); ++ } ++ ++ public void checkExpected(MessageBytes mb) { ++ checkExpected.accept(mb); ++ } ++ ++ public void checkNull(MessageBytes mb) { ++ checkNull.accept(mb); ++ } ++ } ++ ++ ++ private static List> permutations(List items) { ++ List> results = new ArrayList<>(); ++ ++ if (items.size() == 1) { ++ results.add(items); ++ } else { ++ List others = new ArrayList<>(items); ++ T first = others.remove(0); ++ List> subPermutations = permutations(others); ++ ++ for (List subPermutation : subPermutations) { ++ for (int i = 0; i <= subPermutation.size(); i++) { ++ List result = new ArrayList<>(subPermutation); ++ result.add(i, first); ++ results.add(result); ++ } ++ } ++ } ++ ++ return results; ++ } ++} + diff --git a/SOURCES/rhbz-2362782.patch b/SOURCES/rhbz-2362782.patch new file mode 100644 index 0000000..775f439 --- /dev/null +++ b/SOURCES/rhbz-2362782.patch @@ -0,0 +1,582 @@ +--- java/org/apache/catalina/connector/CoyoteAdapter.java ++++ java/org/apache/catalina/connector/CoyoteAdapter.java +@@ -663,21 +663,19 @@ + 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. +--- java/org/apache/catalina/valves/rewrite/RewriteValve.java ++++ java/org/apache/catalina/valves/rewrite/RewriteValve.java +@@ -21,6 +21,7 @@ + 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 @@ + */ + 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. + */ +@@ -305,22 +324,51 @@ + + 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; ++ ++ // 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 +378,7 @@ + if (rule.isHost()) { + host = newtest; + } else { +- urlDecoded = newtest; ++ urlRewriteEncoded = newtest; + } + rewritten = true; + } +@@ -363,29 +411,30 @@ + 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); ++ } ++ ++ // Step 3. Complete the 2nd stage to encoding. + StringBuilder urlStringEncoded = +- new StringBuilder(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset)); +- if (!qsd && originalQueryStringEncoded != null +- && originalQueryStringEncoded.length() > 0) { +- if (rewrittenQueryStringDecoded == null) { ++ new StringBuilder(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset)); ++ ++ if (!qsd && originalQueryStringEncoded != null && !originalQueryStringEncoded.isEmpty()) { ++ 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 @@ + 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,12 +528,12 @@ + 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; +@@ -498,24 +547,25 @@ + // 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, String.valueOf(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)); +- if (qsa && originalQueryStringEncoded != null && +- originalQueryStringEncoded.length() > 0) { ++ chunk.append(REWRITE_QUERY_ENCODER.encode(queryStringRewriteEncoded, uriCharset)); ++ if (qsa && originalQueryStringEncoded != null && !originalQueryStringEncoded.isEmpty()) { + chunk.append('&'); + chunk.append(originalQueryStringEncoded); + } +@@ -786,4 +836,31 @@ + 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; ++ } ++ } + } +--- test/org/apache/catalina/valves/rewrite/TestRewriteValve.java ++++ test/org/apache/catalina/valves/rewrite/TestRewriteValve.java +@@ -17,10 +17,13 @@ + 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; + import java.util.Map; ++ ++import javax.servlet.http.HttpServletResponse; + + import org.junit.Assert; + import org.junit.Test; +@@ -50,7 +53,7 @@ + + @Test + public void testBackslashPercentSign() throws Exception { +- doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%255A"); ++ doTestRewrite("RewriteRule ^(.*) /a/\\%5A", "/", "/a/%5A"); + } + + @Test +@@ -227,7 +230,7 @@ + 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 +238,8 @@ + 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 +254,8 @@ + @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 +270,8 @@ + @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 +298,8 @@ + @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 +315,8 @@ + @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 +351,8 @@ + @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 +367,8 @@ + @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 +403,8 @@ + @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 +428,8 @@ + @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 +472,7 @@ + @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 +486,7 @@ + @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 +652,7 @@ + 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 +723,87 @@ + 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, String.valueOf(StandardCharsets.UTF_8)))); ++ Assert.assertTrue(body, body.contains("REQUEST-QUERY-STRING: " + expectedQueryString)); ++ } + } +--- webapps/docs/changelog.xml ++++ webapps/docs/changelog.xml +@@ -105,6 +105,16 @@ + issues do not "pop up" wrt. others). + --> +
++ ++ ++ ++ 60940: Improve the handling of the META-INF/ and ++ META-INF/MANIFEST.MF entries for Jar files located in ++ /WEB-INF/lib when running a web application from a packed ++ WAR file. (markt) ++ ++ ++ + + + +--- webapps/docs/rewrite.xml ++++ webapps/docs/rewrite.xml +@@ -52,6 +52,28 @@ +

It can also be in the context.xml of a webapp. + The valve will then use a rewrite.config file containing the + rewrite directives, it must be placed in the WEB-INF folder of the web application ++

++ ++
++ ++
++ ++

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.
  • ++
+

+ +
diff --git a/SOURCES/rhbz-2406591.patch b/SOURCES/rhbz-2406591.patch new file mode 100644 index 0000000..854c931 --- /dev/null +++ b/SOURCES/rhbz-2406591.patch @@ -0,0 +1,258 @@ +--- java/org/apache/catalina/valves/rewrite/RewriteValve.java ++++ java/org/apache/catalina/valves/rewrite/RewriteValve.java +@@ -326,9 +326,8 @@ + + // 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(); ++ String queryStringOriginalEncoded = request.getQueryString(); ++ MessageBytes urlMB = context ? request.getRequestPathMB() : request.getDecodedRequestURIMB(); + urlMB.toChars(); + CharSequence urlDecoded = urlMB.getCharChunk(); + +@@ -425,10 +424,10 @@ + StringBuilder urlStringEncoded = + new StringBuilder(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset)); + +- if (!qsd && originalQueryStringEncoded != null && !originalQueryStringEncoded.isEmpty()) { ++ 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 @@ + 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 +@@ -550,24 +549,31 @@ + + // 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 +- urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded); ++ // Rewriting may have denormalized the URL and added encoded characters ++ // Decode then normalize ++ String urlStringRewriteDecoded = URLDecoder.decode(urlStringRewriteEncoded, String.valueOf(uriCharset)); ++ urlStringRewriteDecoded = RequestUtil.normalize(urlStringRewriteDecoded); + 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(URLDecoder.decode(urlStringRewriteEncoded, String.valueOf(uriCharset))); +- // Set the new Query if there is one +- if (queryStringRewriteEncoded != null) { ++ chunk.append(urlStringRewriteDecoded); ++ // 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().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); ++ } ++ } else { ++ // New query string. Therefore the original is dropped unless QSA is defined (and QSD is not). + request.getCoyoteRequest().queryString().setChars(MessageBytes.EMPTY_CHAR_ARRAY, 0, 0); + chunk = request.getCoyoteRequest().queryString().getCharChunk(); + chunk.append(REWRITE_QUERY_ENCODER.encode(queryStringRewriteEncoded, uriCharset)); +- if (qsa && originalQueryStringEncoded != null && !originalQueryStringEncoded.isEmpty()) { ++ if (qsa && queryStringOriginalEncoded != null && !queryStringOriginalEncoded.isEmpty()) { + chunk.append('&'); +- chunk.append(originalQueryStringEncoded); ++ chunk.append(queryStringOriginalEncoded); + } + } + // Set the new host if it changed +@@ -652,6 +658,10 @@ + StringTokenizer flagsTokenizer = new StringTokenizer(flags, ","); + 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; +--- test/org/apache/catalina/startup/TomcatBaseTest.java ++++ test/org/apache/catalina/startup/TomcatBaseTest.java +@@ -551,7 +551,7 @@ + value.append(";"); + } + } +- out.println("PARAM/" + name + ": " + value); ++ out.println("PARAM:" + name + ": " + value); + } + + out.println("SESSION-REQUESTED-ID: " + +--- test/org/apache/catalina/valves/rewrite/TestRewriteValve.java ++++ test/org/apache/catalina/valves/rewrite/TestRewriteValve.java +@@ -180,17 +180,112 @@ + } + + @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?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 testQueryStringRemove02() throws Exception { ++ public void testQueryStringRemoveInvalid02() throws Exception { + doTestRewrite("RewriteRule ^/b/(.*) /c/$1 [QSD]", "/b/d?=1", "/c/d", null); + } + +@@ -511,9 +606,8 @@ + @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"); + } + + +@@ -669,12 +763,16 @@ + // 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); + } +--- webapps/docs/changelog.xml ++++ webapps/docs/changelog.xml +@@ -113,6 +113,10 @@ + /WEB-INF/lib when running a web application from a packed + WAR file. (markt) + ++ ++ Fix handling of QSA and QSD flags in ++ RewriteValve. (markt) ++ + + + diff --git a/SOURCES/tomcat-9.0-digest.script b/SOURCES/tomcat-9.0-digest.script old mode 100644 new mode 100755 diff --git a/SOURCES/tomcat-9.0-tool-wrapper.script b/SOURCES/tomcat-9.0-tool-wrapper.script old mode 100644 new mode 100755 diff --git a/SOURCES/tomcat-9.0.wrapper b/SOURCES/tomcat-9.0.wrapper old mode 100644 new mode 100755 diff --git a/SOURCES/tomcat-functions b/SOURCES/tomcat-functions old mode 100644 new mode 100755 diff --git a/SOURCES/tomcat-preamble b/SOURCES/tomcat-preamble old mode 100644 new mode 100755 diff --git a/SOURCES/tomcat-server b/SOURCES/tomcat-server old mode 100644 new mode 100755 diff --git a/SPECS/pki-servlet-engine.spec b/SPECS/pki-servlet-engine.spec index a0623d2..1029487 100644 --- a/SPECS/pki-servlet-engine.spec +++ b/SPECS/pki-servlet-engine.spec @@ -58,7 +58,7 @@ Name: pki-servlet-engine Epoch: 1 Version: %{major_version}.%{minor_version}.%{micro_version} -Release: 1%{?dist}.2 +Release: 1%{?dist}.3 Summary: Apache Servlet/JSP Engine, RI for Servlet %{servletspec}/JSP %{jspspec} API Group: System Environment/Daemons License: ASL 2.0 @@ -83,6 +83,9 @@ Patch2: tomcat-%{major_version}.%{minor_version}-catalina-policy.patch Patch3: exclude-OSGi-metadata.patch Patch4: rhbz-2314686.patch Patch5: rhbz-2332817.patch +Patch6: message-byte-conversion.patch +Patch7: rhbz-2362782.patch +Patch8: rhbz-2406591.patch BuildArch: noarch @@ -147,6 +150,9 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name " %patch3 -p0 %patch4 -p0 %patch5 -p0 +%patch6 -p0 +%patch7 -p0 +%patch8 -p0 # Since we don't support ECJ in RHEL anymore, remove the class that requires it %{__rm} -f java/org/apache/jasper/compiler/JDTCompiler.java @@ -389,6 +395,12 @@ fi %{_javadir}/tomcat-servlet-%{servletspec}*.jar %changelog +* Wed Dec 10 2025 Adam Krajcik - 1:9.0.50-1.el9_2.3 +- Resolves: RHEL-124509 + pki-servlet-engine: Directory traversal via rewrite with possible RCE (CVE-2025-55752) +- Resolves: RHEL-91744 + pki-servlet-engine: Bypass of rules in Rewrite Valve (CVE-2025-31651) + * Fri Jan 17 2025 Dimitris Soumis - 1:9.0.50-1.el9_2.2 - Resolves: RHEL-71715 pki-servlet-engine: RCE due to TOCTOU issue in JSP compilation (CVE-2024-50379)