This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new 156eacc58b Fix BZ 70208 - Make URL encoding more robust.
156eacc58b is described below
commit 156eacc58bc694569a9d544925bb8898ae4e2925
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Sep 9 09:59:49 2026 +0100
Fix BZ 70208 - Make URL encoding more robust.
Based on pull request #1065 by Chenjp
---
java/org/apache/catalina/util/RequestUtil.java | 25 ++++++++++---
.../apache/catalina/connector/TestResponse.java | 41 +++++++++++++++++++---
test/org/apache/tomcat/unittest/TesterRequest.java | 14 +++++++-
.../tomcat/unittest/TesterServletContext.java | 41 +++++++++++-----------
webapps/docs/changelog.xml | 4 +++
5 files changed, 96 insertions(+), 29 deletions(-)
diff --git a/java/org/apache/catalina/util/RequestUtil.java
b/java/org/apache/catalina/util/RequestUtil.java
index b9a003620e..58f7ab9b29 100644
--- a/java/org/apache/catalina/util/RequestUtil.java
+++ b/java/org/apache/catalina/util/RequestUtil.java
@@ -17,11 +17,13 @@
package org.apache.catalina.util;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.connector.Request;
+import org.apache.tomcat.util.buf.UDecoder;
/**
* General purpose request parsing and encoding utility methods.
@@ -148,14 +150,29 @@ public final class RequestUtil {
}
/*
- * This isn't perfect but is the best that can be done without running
the full mapping logic on the url to
- * determine which web application that url will map to.
+ * May not be perfect, but make a best efforts attempt to determine
whether the URL belongs to current request
+ * or not.
+ *
+ * Stripping of path parameters, decoding and normalization should all
be unnecessary but are included here as
+ * hardening against the application using untrusted data when
constructing the URL.
*/
- if
(!url.getPath().startsWith(request.getServletContext().getContextPath())) {
+ String urlPath = url.getPath();
+ urlPath = stripPathParams(urlPath, null);
+ urlPath = UDecoder.URLDecode(urlPath, StandardCharsets.UTF_8);
+ urlPath = org.apache.tomcat.util.http.RequestUtil.normalize(urlPath);
+ if (urlPath == null) {
+ // Normalization failed. Path tried to escape the root.
return false;
}
- return true;
+ // Context path will not end with "/"
+ String requestContextPath =
request.getServletContext().getContextPath();
+
+ if (urlPath.equals(requestContextPath) ||
urlPath.startsWith(requestContextPath + "/")) {
+ return true;
+ }
+
+ return false;
}
diff --git a/test/org/apache/catalina/connector/TestResponse.java
b/test/org/apache/catalina/connector/TestResponse.java
index 98649fe66d..1a81836d93 100644
--- a/test/org/apache/catalina/connector/TestResponse.java
+++ b/test/org/apache/catalina/connector/TestResponse.java
@@ -349,7 +349,12 @@ public class TestResponse extends TomcatBaseTest {
private void doTestEncodeURL(String location, String expected) {
- Request req = new TesterRequest(true);
+ doTestEncodeURL("", location, expected);
+ }
+
+
+ private void doTestEncodeURL(String currentContextPath, String location,
String expected) {
+ Request req = new TesterRequest(true, "/level1/level2/foo.html",
currentContextPath);
req.setRequestedSessionId("1234");
req.setRequestedSessionURL(true);
Response resp = new Response();
@@ -455,6 +460,31 @@ public class TestResponse extends TomcatBaseTest {
}
+ @Test
+ public void testEncodeURLBug70208a() throws Exception {
+ doTestEncodeURL("/admin", "/admin/index",
"/admin/index;jsessionid=1234");
+ }
+
+
+ @Test
+ public void testEncodeURLBug70208b() throws Exception {
+ doTestEncodeURL("/admin", "/admin/../public/index",
"/admin/../public/index");
+ }
+
+
+ @Test
+ public void testEncodeURLBug70208c() throws Exception {
+ doTestEncodeURL("/admin", "/public/..;/admin/index;jsessionid=zzz",
+ "/public/..;/admin/index;jsessionid=zzz;jsessionid=1234");
+ }
+
+
+ @Test
+ public void testEncodeURLBug70208d() throws Exception {
+ doTestEncodeURL("/admin", "/administrator/index",
"/administrator/index");
+ }
+
+
private void doTestEncodeRedirectURL(String location, String expected) {
Request req = new TesterRequest(true);
req.setRequestedSessionId("1234");
@@ -1046,21 +1076,24 @@ public class TestResponse extends TomcatBaseTest {
response.setHeader("Content-Length", "10");
Assert.assertEquals(10, response.getContentLength());
Assert.assertEquals("10", response.getHeader("Content-Length"));
- Assert.assertEquals(1, response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
+ Assert.assertEquals(1,
+ response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
Assert.assertEquals(1, response.getHeaders("Content-Length").size());
// Invalid
response.setHeader("Content-Length", "zzz");
Assert.assertEquals(-1, response.getContentLength());
Assert.assertNull(response.getHeader("Content-Length"));
- Assert.assertEquals(0, response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
+ Assert.assertEquals(0,
+ response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
Assert.assertEquals(0, response.getHeaders("Content-Length").size());
// Valid
response.setHeader("Content-Length", "20");
Assert.assertEquals(20, response.getContentLength());
Assert.assertEquals("20", response.getHeader("Content-Length"));
- Assert.assertEquals(1, response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
+ Assert.assertEquals(1,
+ response.getHeaderNames().stream().filter(s ->
s.equalsIgnoreCase("Content-Length")).count());
Assert.assertEquals(1, response.getHeaders("Content-Length").size());
}
}
diff --git a/test/org/apache/tomcat/unittest/TesterRequest.java
b/test/org/apache/tomcat/unittest/TesterRequest.java
index c21882c102..93cae8d8a1 100644
--- a/test/org/apache/tomcat/unittest/TesterRequest.java
+++ b/test/org/apache/tomcat/unittest/TesterRequest.java
@@ -56,9 +56,14 @@ public class TesterRequest extends Request {
public TesterRequest(boolean withSession, String requestUri) {
+ this(withSession, requestUri, "");
+ }
+
+
+ public TesterRequest(boolean withSession, String requestUri, String
reqContextPath) {
super(null);
context = new TesterContext();
- servletContext = new TesterServletContext();
+ servletContext = new TesterServletContext(reqContextPath);
context.setServletContext(servletContext);
if (withSession) {
Set<SessionTrackingMode> modes = new HashSet<>();
@@ -78,11 +83,13 @@ public class TesterRequest extends Request {
return "http";
}
+
@Override
public String getServerName() {
return "localhost";
}
+
@Override
public int getServerPort() {
return 8080;
@@ -123,18 +130,22 @@ public class TesterRequest extends Request {
private String method;
+
public void setMethod(String method) {
this.method = method;
}
+
@Override
public String getMethod() {
return method;
}
private final Map<String,List<String>> headers = new HashMap<>();
+
public void addHeader(String name, String value) {
headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value);
}
+
@Override
public String getHeader(String name) {
List<String> values = headers.get(name);
@@ -143,6 +154,7 @@ public class TesterRequest extends Request {
}
return values.get(0);
}
+
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = headers.get(name);
diff --git a/test/org/apache/tomcat/unittest/TesterServletContext.java
b/test/org/apache/tomcat/unittest/TesterServletContext.java
index e2d72ca567..4a4d236b30 100644
--- a/test/org/apache/tomcat/unittest/TesterServletContext.java
+++ b/test/org/apache/tomcat/unittest/TesterServletContext.java
@@ -43,14 +43,19 @@ import org.apache.tomcat.util.descriptor.web.FilterDef;
public class TesterServletContext implements ServletContext {
- /**
- * {@inheritDoc}
- * <p>
- * This test implementation is hard coded to return an empty String.
- */
+ private String contextPath = "";
+
+ public TesterServletContext() {
+ this("");
+ }
+
+ public TesterServletContext(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
@Override
public String getContextPath() {
- return "";
+ return contextPath;
}
/**
@@ -66,8 +71,7 @@ public class TesterServletContext implements ServletContext {
/**
* {@inheritDoc}
* <p>
- * This test implementation is hard coded to return the class loader that
- * loaded this class.
+ * This test implementation is hard coded to return the class loader that
loaded this class.
*/
@Override
public ClassLoader getClassLoader() {
@@ -221,8 +225,7 @@ public class TesterServletContext implements ServletContext
{
}
@Override
- public Dynamic addServlet(String servletName,
- Class<? extends Servlet> servletClass) {
+ public Dynamic addServlet(String servletName, Class<? extends Servlet>
servletClass) {
throw new RuntimeException("Not implemented");
}
@@ -232,8 +235,7 @@ public class TesterServletContext implements ServletContext
{
}
@Override
- public <T extends Servlet> T createServlet(Class<T> c)
- throws ServletException {
+ public <T extends Servlet> T createServlet(Class<T> c) throws
ServletException {
throw new RuntimeException("Not implemented");
}
@@ -243,7 +245,7 @@ public class TesterServletContext implements ServletContext
{
}
@Override
- public Map<String, ? extends ServletRegistration>
getServletRegistrations() {
+ public Map<String,? extends ServletRegistration> getServletRegistrations()
{
throw new RuntimeException("Not implemented");
}
@@ -263,8 +265,7 @@ public class TesterServletContext implements ServletContext
{
}
@Override
- public <T extends Filter> T createFilter(Class<T> c)
- throws ServletException {
+ public <T extends Filter> T createFilter(Class<T> c) throws
ServletException {
throw new RuntimeException("Not implemented");
}
@@ -274,20 +275,21 @@ public class TesterServletContext implements
ServletContext {
}
@Override
- public Map<String, ? extends FilterRegistration> getFilterRegistrations() {
+ public Map<String,? extends FilterRegistration> getFilterRegistrations() {
throw new RuntimeException("Not implemented");
}
private SessionCookieConfig sessionCookieConfig = new
TesterSessionCookieConfig();
+
@Override
public SessionCookieConfig getSessionCookieConfig() {
return sessionCookieConfig;
}
private final Set<SessionTrackingMode> sessionTrackingModes = new
HashSet<>();
+
@Override
- public void setSessionTrackingModes(
- Set<SessionTrackingMode> sessionTrackingModes) {
+ public void setSessionTrackingModes(Set<SessionTrackingMode>
sessionTrackingModes) {
this.sessionTrackingModes.clear();
this.sessionTrackingModes.addAll(sessionTrackingModes);
}
@@ -318,8 +320,7 @@ public class TesterServletContext implements ServletContext
{
}
@Override
- public <T extends EventListener> T createListener(Class<T> c)
- throws ServletException {
+ public <T extends EventListener> T createListener(Class<T> c) throws
ServletException {
throw new RuntimeException("Not implemented");
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 641c970b8f..f863a6232b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -213,6 +213,10 @@
Require the request to the login action during FORM authentication to
be
made using HTTP POST. (markt)
</fix>
+ <fix>
+ <bug>70208</bug>: Make URL encoding more robust. Based on pull request
+ <pr>1065</pr> by Chenjp. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]