This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new c1b011d7b8 Add attributes to Context to control decoding
RequestDispatcher path
c1b011d7b8 is described below
commit c1b011d7b868bbcc5856731774b729da2c108de5
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Jan 23 12:08:56 2025 +0000
Add attributes to Context to control decoding RequestDispatcher path
Adds encodedReverseSolidusHandling and encodedSolidusHandling. Both
default to DECODE (the current behaviour)
---
java/org/apache/catalina/Context.java | 67 +++++++++
.../apache/catalina/core/ApplicationContext.java | 3 +-
java/org/apache/catalina/core/StandardContext.java | 41 ++++++
.../apache/tomcat/util/buf/LocalStrings.properties | 2 +
java/org/apache/tomcat/util/buf/UDecoder.java | 99 +++++++++++--
...estApplicationContextGetRequestDispatcherC.java | 161 +++++++++++++++++++++
webapps/docs/changelog.xml | 6 +
webapps/docs/config/context.xml | 40 +++++
8 files changed, 404 insertions(+), 15 deletions(-)
diff --git a/java/org/apache/catalina/Context.java
b/java/org/apache/catalina/Context.java
index 9c59c10a9c..9cc19c5369 100644
--- a/java/org/apache/catalina/Context.java
+++ b/java/org/apache/catalina/Context.java
@@ -25,6 +25,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContainerInitializer;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
@@ -36,6 +37,7 @@ import org.apache.catalina.deploy.NamingResourcesImpl;
import org.apache.tomcat.ContextBind;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.JarScanner;
+import org.apache.tomcat.util.buf.EncodedSolidusHandling;
import org.apache.tomcat.util.descriptor.web.ApplicationParameter;
import org.apache.tomcat.util.descriptor.web.ErrorPage;
import org.apache.tomcat.util.descriptor.web.FilterDef;
@@ -1872,4 +1874,69 @@ public interface Context extends Container, ContextBind {
return ConfigFileLoader.getSource().getResource(name);
}
+
+ /**
+ * Obtain the current configuration for the handling of encoded reverse
solidus (%5c - \) characters in paths used
+ * to obtain {@link RequestDispatcher} instances for this {@link Context}.
+ *
+ * @return Obtain the current configuration for the handling of encoded
reverse solidus characters
+ */
+ default String getEncodedReverseSolidusHandling() {
+ return EncodedSolidusHandling.DECODE.getValue();
+ }
+
+
+ /**
+ * Configure the handling for encoded reverse solidus (%5c - \) characters
in paths used to obtain
+ * {@link RequestDispatcher} instances for this {@link Context}.
+ *
+ * @param encodedReverseSolidusHandling One of the values of {@link
EncodedSolidusHandling}
+ */
+ default void setEncodedReverseSolidusHandling(String
encodedReverseSolidusHandling) {
+ throw new UnsupportedOperationException();
+ }
+
+
+ /**
+ * Obtain the current configuration for the handling of encoded reverse
solidus (%5c - \) characters in paths used
+ * to obtain {@link RequestDispatcher} instances for this {@link Context}.
+ *
+ * @return Obtain the current configuration for the handling of encoded
reverse solidus characters
+ */
+ default EncodedSolidusHandling getEncodedReverseSolidusHandlingEnum() {
+ return EncodedSolidusHandling.DECODE;
+ }
+
+
+ /**
+ * Obtain the current configuration for the handling of encoded solidus
(%2f - /) characters in paths used to obtain
+ * {@link RequestDispatcher} instances for this {@link Context}.
+ *
+ * @return Obtain the current configuration for the handling of encoded
solidus characters
+ */
+ default String getEncodedSolidusHandling() {
+ return EncodedSolidusHandling.DECODE.getValue();
+ }
+
+
+ /**
+ * Configure the handling for encoded solidus (%2f - /) characters in
paths used to obtain {@link RequestDispatcher}
+ * instances for this {@link Context}.
+ *
+ * @param encodedSolidusHandling One of the values of {@link
EncodedSolidusHandling}
+ */
+ default void setEncodedSolidusHandling(String encodedSolidusHandling) {
+ throw new UnsupportedOperationException();
+ }
+
+
+ /**
+ * Obtain the current configuration for the handling of encoded solidus
(%2f - /) characters in paths used to obtain
+ * {@link RequestDispatcher} instances for this {@link Context}.
+ *
+ * @return Obtain the current configuration for the handling of encoded
solidus characters
+ */
+ default EncodedSolidusHandling getEncodedSolidusHandlingEnum() {
+ return EncodedSolidusHandling.DECODE;
+ }
}
diff --git a/java/org/apache/catalina/core/ApplicationContext.java
b/java/org/apache/catalina/core/ApplicationContext.java
index 353a16b597..5fc0ea14e0 100644
--- a/java/org/apache/catalina/core/ApplicationContext.java
+++ b/java/org/apache/catalina/core/ApplicationContext.java
@@ -378,7 +378,8 @@ public class ApplicationContext implements ServletContext {
// Decode only if the uri derived from the provided path is expected
to be encoded
if (getContext().getDispatchersUseEncodedPaths()) {
- uriToMap = UDecoder.URLDecode(uriToMap, StandardCharsets.UTF_8);
+ uriToMap = UDecoder.URLDecode(uriToMap, StandardCharsets.UTF_8,
context.getEncodedSolidusHandlingEnum(),
+ context.getEncodedReverseSolidusHandlingEnum());
}
// Then normalize
diff --git a/java/org/apache/catalina/core/StandardContext.java
b/java/org/apache/catalina/core/StandardContext.java
index 2994082059..a06014d052 100644
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -114,6 +114,7 @@ import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.InstanceManagerBindings;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.EncodedSolidusHandling;
import org.apache.tomcat.util.buf.StringUtils;
import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.descriptor.XmlIdentifiers;
@@ -800,9 +801,49 @@ public class StandardContext extends ContainerBase
implements Context, Notificat
private int notFoundClassResourceCacheSize = 1000;
+ private EncodedSolidusHandling encodedReverseSolidusHandling =
EncodedSolidusHandling.DECODE;
+
+ private EncodedSolidusHandling encodedSolidusHandling =
EncodedSolidusHandling.DECODE;
+
// ----------------------------------------------------- Context Properties
+ @Override
+ public String getEncodedReverseSolidusHandling() {
+ return encodedReverseSolidusHandling.getValue();
+ }
+
+
+ @Override
+ public void setEncodedReverseSolidusHandling(String
encodedReverseSolidusHandling) {
+ this.encodedReverseSolidusHandling =
EncodedSolidusHandling.fromString(encodedReverseSolidusHandling);
+ }
+
+
+ @Override
+ public EncodedSolidusHandling getEncodedReverseSolidusHandlingEnum() {
+ return encodedReverseSolidusHandling;
+ }
+
+
+ @Override
+ public String getEncodedSolidusHandling() {
+ return encodedSolidusHandling.getValue();
+ }
+
+
+ @Override
+ public void setEncodedSolidusHandling(String encodedSolidusHandling) {
+ this.encodedSolidusHandling =
EncodedSolidusHandling.fromString(encodedSolidusHandling);
+ }
+
+
+ @Override
+ public EncodedSolidusHandling getEncodedSolidusHandlingEnum() {
+ return encodedSolidusHandling;
+ }
+
+
public int getNotFoundClassResourceCacheSize() {
return notFoundClassResourceCacheSize;
}
diff --git a/java/org/apache/tomcat/util/buf/LocalStrings.properties
b/java/org/apache/tomcat/util/buf/LocalStrings.properties
index 56939e14e5..cbef1ff927 100644
--- a/java/org/apache/tomcat/util/buf/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/buf/LocalStrings.properties
@@ -47,3 +47,5 @@ uDecoder.noBackslash=The encoded backslash character is not
allowed
uDecoder.noSlash=The encoded slash character is not allowed
uDecoder.urlDecode.conversionError=Failed to decode [{0}] using character set
[{1}]
uDecoder.urlDecode.missingDigit=Failed to decode [{0}] because the % character
must be followed by two hexadecimal digits
+uDecoder.urlDecode.rejectEncodedReverseSolidus=Failed to decode [{0}] because
it contained a %5c sequence the decoder is configured to reject
+uDecoder.urlDecode.rejectEncodedSolidus=Failed to decode [{0}] because it
contained a %2f sequence the decoder is configured to reject
diff --git a/java/org/apache/tomcat/util/buf/UDecoder.java
b/java/org/apache/tomcat/util/buf/UDecoder.java
index 3a1fa6edbf..da63fa030c 100644
--- a/java/org/apache/tomcat/util/buf/UDecoder.java
+++ b/java/org/apache/tomcat/util/buf/UDecoder.java
@@ -169,20 +169,20 @@ public final class UDecoder {
}
}
} else if (res == '\\') {
- switch (encodedReverseSolidusHandling) {
- case DECODE: {
- buff[idx] = (byte) res;
- break;
- }
- case REJECT: {
- throw EXCEPTION_BACKSLASH;
- }
- case PASS_THROUGH: {
- buff[idx++] = buff[j - 2];
- buff[idx++] = buff[j - 1];
- buff[idx] = buff[j];
- }
+ switch (encodedReverseSolidusHandling) {
+ case DECODE: {
+ buff[idx] = (byte) res;
+ break;
+ }
+ case REJECT: {
+ throw EXCEPTION_BACKSLASH;
}
+ case PASS_THROUGH: {
+ buff[idx++] = buff[j - 2];
+ buff[idx++] = buff[j - 1];
+ buff[idx] = buff[j];
+ }
+ }
} else if (res == '%') {
/*
* If encoded '/' or '\' is going to be left encoded then
so must encoded '%' else the subsequent
@@ -218,6 +218,24 @@ public final class UDecoder {
* @exception IllegalArgumentException if a '%' character is not followed
by a valid 2-digit hexadecimal number
*/
public static String URLDecode(String str, Charset charset) {
+ return URLDecode(str, charset, EncodedSolidusHandling.DECODE,
EncodedSolidusHandling.DECODE);
+ }
+
+
+ /**
+ * Decode and return the specified URL-encoded String. It is assumed the
string is not a query string.
+ *
+ * @param str The url-encoded string
+ * @param charset The character encoding to use; if
null, UTF-8 is used.
+ * @param encodedSolidusHandling The required handling of encoded
solidus (%2f - /)
+ * @param encodedReverseSolidusHandling The required handling of encoded
reverse solidus (%5c - \)
+ *
+ * @return the decoded string
+ *
+ * @exception IllegalArgumentException if a '%' character is not followed
by a valid 2-digit hexadecimal number
+ */
+ public static String URLDecode(String str, Charset charset,
EncodedSolidusHandling encodedSolidusHandling,
+ EncodedSolidusHandling encodedReverseSolidusHandling) {
if (str == null) {
return null;
}
@@ -266,7 +284,60 @@ public final class UDecoder {
char c1 = sourceChars[ix++];
char c2 = sourceChars[ix++];
if (isHexDigit(c1) && isHexDigit(c2)) {
- baos.write(x2c(c1, c2));
+ int decoded = x2c(c1, c2);
+ switch (decoded) {
+ case '/': {
+ switch (encodedSolidusHandling) {
+ case DECODE: {
+ osw.append('/');
+ break;
+ }
+ case PASS_THROUGH: {
+ osw.append(c);
+ osw.append(c1);
+ osw.append(c2);
+ break;
+ }
+ case REJECT: {
+ throw new IllegalArgumentException(
+
sm.getString("uDecoder.urlDecode.rejectEncodedSolidus", str));
+ }
+ }
+ break;
+ }
+ case '\\': {
+ switch (encodedReverseSolidusHandling) {
+ case DECODE: {
+ osw.append('\\');
+ break;
+ }
+ case PASS_THROUGH: {
+ osw.append(c);
+ osw.append(c1);
+ osw.append(c2);
+ break;
+ }
+ case REJECT: {
+ throw new IllegalArgumentException(
+
sm.getString("uDecoder.urlDecode.rejectEncodedReverseSolidus", str));
+ }
+ }
+ break;
+ }
+ case '%': {
+ if (encodedReverseSolidusHandling ==
EncodedSolidusHandling.PASS_THROUGH ||
+ encodedSolidusHandling ==
EncodedSolidusHandling.PASS_THROUGH) {
+ osw.append(c);
+ osw.append(c1);
+ osw.append(c2);
+ } else {
+ baos.write('%');
+ }
+ break;
+ }
+ default:
+ baos.write(decoded);
+ }
} else {
throw new
IllegalArgumentException(sm.getString("uDecoder.urlDecode.missingDigit", str));
}
diff --git
a/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
new file mode 100644
index 0000000000..ff5fd02d21
--- /dev/null
+++
b/test/org/apache/catalina/core/TestApplicationContextGetRequestDispatcherC.java
@@ -0,0 +1,161 @@
+/*
+ * 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.catalina.core;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+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;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+
+/*
+ * Tests interaction of ServletContext.getRequestDispatcher() and the
Connector/Context attributes
+ * encodedSolidusHandling and encodedReverseSolidusHandling.
+ */
+@RunWith(value = Parameterized.class)
+public class TestApplicationContextGetRequestDispatcherC extends
TomcatBaseTest {
+
+ @Parameters(name = "{index}: pathInfoRequest[{0}],
pathInfoDispatcher[{1}]")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][]{
+ // Defaults
+ { "/a/b/c", "/a/b/c", null, null },
+ { "/a%2fb%5cc/%25", "/a/b/c/%", "decode", "decode" },
+ { "/a%2fb%5cc/%25", "/a/b%5cc/%25", "decode", "passthrough" },
+ { "/a%2fb%5cc/%25", "/a%2fb/c/%25", "passthrough", "decode" },
+ { "/a%2fb%5cc/%25", "/a%2fb%5cc/%25", "passthrough", "passthrough"
},
+ });
+ }
+
+
+ @Parameter(0)
+ public String pathInfoRequest;
+ @Parameter(1)
+ public String pathInfoDispatcher;
+ @Parameter(2)
+ public String encodedSolidusHandling;
+ @Parameter(3)
+ public String encodedReverseSolidusHandling;
+
+
+ @Test
+ public void testSomething() throws Exception {
+ doTest();
+ }
+
+
+ private void doTest() throws Exception {
+ // Setup Tomcat instance
+ Tomcat tomcat = getTomcatInstance();
+
+ // Configure the Connector
+ tomcat.getConnector().setAllowBackslash(true);
+ if (encodedSolidusHandling != null) {
+
tomcat.getConnector().setEncodedSolidusHandling(encodedSolidusHandling);
+ }
+ if (encodedReverseSolidusHandling != null) {
+
tomcat.getConnector().setEncodedReverseSolidusHandling(encodedReverseSolidusHandling);
+ }
+
+ // No file system docBase required
+ Context ctx = tomcat.addContext("/test", null);
+ ctx.addWelcomeFile("index.html");
+ if (encodedSolidusHandling != null) {
+ ctx.setEncodedSolidusHandling(encodedSolidusHandling);
+ }
+ if (encodedReverseSolidusHandling != null) {
+
ctx.setEncodedReverseSolidusHandling(encodedReverseSolidusHandling);
+ }
+
+ // Servlet that performs a dispatch to ...
+ Tomcat.addServlet(ctx, "rd", new Dispatch(pathInfoRequest));
+ ctx.addServletMappingDecoded("/dispatch/*", "rd");
+
+ // ... this Servlet
+ Tomcat.addServlet(ctx, "target", new Target());
+ ctx.addServletMappingDecoded("/target/*", "target");
+
+ tomcat.start();
+
+ StringBuilder url = new StringBuilder("http://localhost:");
+ url.append(getPort());
+ url.append("/test/dispatch");
+ url.append(pathInfoRequest);
+
+ ByteChunk bc = getUrl(url.toString());
+ String body = bc.toString();
+
+ Assert.assertEquals(pathInfoDispatcher + pathInfoDispatcher, body);
+ }
+
+
+ private static class Dispatch extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String dispatchPath;
+
+ Dispatch(String dispatchPath) {
+ this.dispatchPath = dispatchPath;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+ /*
+ * The original pathInfo as presented by the client and the
dispatchPath are currently the same for this
+ * test, as is the handling configured in the Connector and
Context. The test checks both the normal request
+ * processing and the dispatch processing for the correct handling
of %2f and %5c. The test could be
+ * expanded so different settings are used for the Connector and
the Context.
+ */
+ // Handle an edge case - the application needs to encode any paths
used to obtain a RequestDispatcher
+ String pathInfo = req.getPathInfo();
+ if (pathInfo.endsWith("%")) {
+ pathInfo = pathInfo.replace("%", "%25");
+ }
+ req.getServletContext().getRequestDispatcher("/target" + pathInfo
+ dispatchPath).forward(req, resp);
+ }
+ }
+
+
+ private static class Target extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding(StandardCharsets.UTF_8);
+ resp.getWriter().print(req.getPathInfo());
+ }
+ }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d68b16333e..48a37608ba 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -170,6 +170,12 @@
the processing of the provided path is consistent with normal request
processing. (markt)
</scode>
+ <add>
+ Add <code>encodedReverseSolidusHandling</code> and
+ <code>encodedSolidusHandling</code> attributes to Context to provide
+ control over the handling of the path used to created a
+ <code>RequestDispatcher</code>. (markt)
+ </add>
</changelog>
</subsection>
<subsection name="Coyote">
diff --git a/webapps/docs/config/context.xml b/webapps/docs/config/context.xml
index ecf38f715d..a2fad26af6 100644
--- a/webapps/docs/config/context.xml
+++ b/webapps/docs/config/context.xml
@@ -409,6 +409,46 @@
sufficient.</p>
</attribute>
+ <attribute name="encodedReverseSolidusHandling" required="false">
+ <p>When set to <code>reject</code>, an attempt to obtain a
+ <code>RequestDispatcher</code> for a path containing a <code>%5c</code>
+ sequence will trigger an <code>IllegalArgumentException</code>. When
set
+ to <code>decode</code>, any <code>%5c</code> sequence in the path used
+ to obtain a <code>RequestDispatcher</code> will have that sequence
+ decoded to <code>\</code> (and then normalized to <code>/</code>)
before
+ the <code>RequestDispatcher</code> is created. When set to
+ <code>passthrough</code>, any <code>%5c</code> sequence in the path
used
+ to obtain a <code>RequestDispatcher</code> will remain unchanged.</p>
+ <p>If <code>passthrough</code> is used then it is the application's
+ resposibility to perform any further <code>%nn</code> decoding
required.
+ Any <code>%25</code> sequences (encoded <code>%</code>) in the path
will
+ also be processed with the <code>%25</code> sequence unchanged
+ to avoid potential corruption and/or decoding failure when the path is
+ subsequently <code>%nn</code> decoded by the application.</p>
+ <p>If not specified, the default value is <code>decode</code>.</p>
+ </attribute>
+
+ <attribute name="encodedSolidusHandling" required="false">
+ <p>When set to <code>reject</code>, an attempt to obtain a
+ <code>RequestDispatcher</code> for a path containing a <code>%2f</code>
+ sequence will trigger an <code>IllegalArgumentException</code>. When
set
+ to <code>decode</code>, any <code>%2f</code> sequence in the path used
+ to obtain a <code>RequestDispatcher</code> will have that sequence
+ decoded to <code>/</code> before the <code>RequestDispatcher</code> is
+ created. When set to <code>passthrough</code>, any <code>%2f</code>
+ sequence in the path used to obtain a <code>RequestDispatcher</code>
+ will remain unchanged.</p>
+ <p>If <code>passthrough</code> is used then it is the application's
+ resposibility to perform any further <code>%nn</code> decoding
required.
+ Any <code>%25</code> sequences (encoded <code>%</code>) in the path
will
+ also be processed with the <code>%25</code> sequence unchanged
+ to avoid potential corruption and/or decoding failure when the path is
+ subsequently <code>%nn</code> decoded by the application.</p>
+ <p>If not specified, the default value is <code>decode</code>. This
+ default will change to <code>reject</code> (to align with the
+ <strong>Connector</strong>) in Tomcat 12.</p>
+ </attribute>
+
<attribute name="failCtxIfServletStartFails" required="false">
<p>Set to <code>true</code> to have the context fail its startup if any
servlet that has load-on-startup >=0 fails its own startup.</p>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]