This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new a633fe7c84 Better handling of URLs with literal ';' and '?' a633fe7c84 is described below commit a633fe7c8427b7aac04bde5c8b977dd51632198f Author: Mark Thomas <ma...@apache.org> AuthorDate: Thu Mar 13 15:06:26 2025 +0000 Better handling of URLs with literal ';' and '?' --- .../apache/catalina/connector/CoyoteAdapter.java | 19 ++- .../catalina/valves/rewrite/RewriteValve.java | 136 ++++++++++++++++----- .../catalina/valves/rewrite/TestRewriteValve.java | 119 +++++++++++++++--- webapps/docs/changelog.xml | 5 + webapps/docs/rewrite.xml | 22 ++++ 5 files changed, 246 insertions(+), 55 deletions(-) diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java index 0fd79acd9c..0b87a9efb3 100644 --- a/java/org/apache/catalina/connector/CoyoteAdapter.java +++ b/java/org/apache/catalina/connector/CoyoteAdapter.java @@ -650,18 +650,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() - - * 'suspicious' URI filtering - if required - has already been performed + * 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 c2dbf63861..60294fc19a 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; @@ -65,6 +66,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. */ @@ -296,22 +315,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 && !Objects.equals(test.toString(), newtest.toString())) { if (containerLog.isTraceEnabled()) { @@ -321,7 +369,7 @@ public class RewriteValve extends ValveBase { if (rule.isHost()) { host = newtest; } else { - urlDecoded = newtest; + urlRewriteEncoded = newtest; } rewritten = true; } @@ -358,28 +406,30 @@ 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); } + // Step 3. Complete the 2nd stage to encoding. StringBuilder urlStringEncoded = - new StringBuilder(URLEncoder.DEFAULT.encode(urlStringDecoded, uriCharset)); + new StringBuilder(REWRITE_DEFAULT_ENCODER.encode(urlStringRewriteEncoded, uriCharset)); + if (!qsd && originalQueryStringEncoded != null && !originalQueryStringEncoded.isEmpty()) { - 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) { @@ -388,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 @@ -469,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; @@ -488,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.isEmpty()) { chunk.append('&'); chunk.append(originalQueryStringEncoded); @@ -790,4 +843,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 da4d1c1052..ce2f7c5d5c 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/changelog.xml b/webapps/docs/changelog.xml index 71f58683bf..d4671c75a7 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -215,6 +215,11 @@ Fix stack trace trimming in <code>ErrorReportValve</code> after removal of the security manager support. (remm) </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"> diff --git a/webapps/docs/rewrite.xml b/webapps/docs/rewrite.xml index 31d2e356d9..c6daf279ab 100644 --- a/webapps/docs/rewrite.xml +++ b/webapps/docs/rewrite.xml @@ -56,6 +56,28 @@ </section> +<section name="Using rewrite rules with special characters"> + + <p>The URL presented to the rewrite valve is the same URL used for request + mapping with any literal <code>'%'</code>, <code>';'</code> and/or + <code>'?'</code> characters encoded in <code>%nn</code> form.</p> + + <p>A rewrite rule that wishes to insert a literal <code>'%'</code>, + <code>';'</code>, <code>'?'</code>, <code>'&'</code> or <code>'='</code> + character should do so in <code>%nn</code> form. Other characters maybe + inserted in either literal or <code>%nn</code> form.</p> + + <p>This enables the rewrite rules to: + <ul> + <li>process URLs containing literal <code>'?'</code> characters;</li> + <li>add a query string;</li> + <li>insert a literal <code>'%'</code> character without it being confused with + <code>%nn</code> encoding.</li> + </ul> + </p> + +</section> + <section name="Directives"> <p>The rewrite.config file contains a list of directives which closely --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org