This is an automated email from the ASF dual-hosted git repository.
markt-asf 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 3dd0776431 Additional context path validation on deployment
3dd0776431 is described below
commit 3dd0776431043308451fbc7ddc14c7b847066bbd
Author: Mark Thomas <[email protected]>
AuthorDate: Tue May 12 12:24:43 2026 +0100
Additional context path validation on deployment
---
.../catalina/manager/HTMLManagerServlet.java | 6 ++
.../apache/catalina/manager/ManagerServlet.java | 4 +-
java/org/apache/catalina/startup/HostConfig.java | 77 ++++++++++------------
.../catalina/startup/LocalStrings.properties | 2 +
java/org/apache/catalina/util/ContextName.java | 14 ++++
webapps/docs/changelog.xml | 4 ++
6 files changed, 60 insertions(+), 47 deletions(-)
diff --git a/java/org/apache/catalina/manager/HTMLManagerServlet.java
b/java/org/apache/catalina/manager/HTMLManagerServlet.java
index 7352922397..e99c5eecfb 100644
--- a/java/org/apache/catalina/manager/HTMLManagerServlet.java
+++ b/java/org/apache/catalina/manager/HTMLManagerServlet.java
@@ -250,6 +250,12 @@ public class HTMLManagerServlet extends ManagerServlet {
}
ContextName cn = new ContextName(filename, true);
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ if (!validateContextName(cn, printWriter, smClient)) {
+ return stringWriter.toString();
+ }
+
String name = cn.getName();
if (host.findChild(name) != null && !isDeployed(name)) {
diff --git a/java/org/apache/catalina/manager/ManagerServlet.java
b/java/org/apache/catalina/manager/ManagerServlet.java
index 0410d26002..65cc87018a 100644
--- a/java/org/apache/catalina/manager/ManagerServlet.java
+++ b/java/org/apache/catalina/manager/ManagerServlet.java
@@ -1583,9 +1583,7 @@ public class ManagerServlet extends HttpServlet
implements ContainerServlet {
*/
protected static boolean validateContextName(ContextName cn, PrintWriter
writer, StringManager smClient) {
- // ContextName should be non-null with a path that is empty or starts
- // with /
- if (cn != null && (cn.getPath().startsWith("/") ||
cn.getPath().isEmpty())) {
+ if (cn != null && cn.isPathValid()) {
return true;
}
diff --git a/java/org/apache/catalina/startup/HostConfig.java
b/java/org/apache/catalina/startup/HostConfig.java
index bcef35da46..c378e3ea86 100644
--- a/java/org/apache/catalina/startup/HostConfig.java
+++ b/java/org/apache/catalina/startup/HostConfig.java
@@ -146,6 +146,16 @@ public class HostConfig implements LifecycleListener {
protected Digester digester = createDigester(contextClass);
private final Object digesterLock = new Object();
+ /**
+ * The list of descriptors in the appBase to be ignored because they are
invalid (e.g. contain /../ sequences).
+ */
+ protected final Set<String> invalidDescriptors = new HashSet<>();
+
+ /**
+ * The list of directories in the appBase to be ignored because they are
invalid (e.g. contain /../ sequences).
+ */
+ protected final Set<String> invalidDirectories = new HashSet<>();
+
/**
* The list of Wars in the appBase to be ignored because they are invalid
(e.g. contain /../ sequences).
*/
@@ -493,7 +503,7 @@ public class HostConfig implements LifecycleListener {
for (String file : files) {
File contextXml = new File(configBase, file);
- if (file.toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
+ if (file.toLowerCase(Locale.ENGLISH).endsWith(".xml") &&
!invalidDescriptors.contains(file)) {
ContextName cn = new ContextName(file, true);
if (tryAddServiced(cn.getName())) {
@@ -535,6 +545,13 @@ public class HostConfig implements LifecycleListener {
@SuppressWarnings("null") // context is not null
protected void deployDescriptor(ContextName cn, File contextXml) {
+ // Check for descriptors with /../ /./ or similar sequences in the name
+ if (!cn.isPathValid()) {
+ log.error(sm.getString("hostConfig.illegalDescriptorName",
contextXml.getName()));
+ invalidDescriptors.add(contextXml.getName());
+ return;
+ }
+
DeployedApplication deployedApp = new
DeployedApplication(cn.getName(), true);
long startTime = 0;
@@ -737,14 +754,6 @@ public class HostConfig implements LifecycleListener {
continue;
}
- // Check for WARs with /../ /./ or similar sequences
in the name
- if (!validateContextPath(appBase, cn.getBaseName())) {
-
log.error(sm.getString("hostConfig.illegalWarName", file));
- invalidWars.add(file);
- removeServiced(cn.getName());
- continue;
- }
-
// DeployWAR will call removeServiced
results.add(es.submit(new DeployWar(this, cn, war)));
} catch (Throwable t) {
@@ -766,40 +775,6 @@ public class HostConfig implements LifecycleListener {
}
- private boolean validateContextPath(File appBase, String contextPath) {
- // More complicated than the ideal as the canonical path may or may
- // not end with File.separator for a directory
-
- StringBuilder docBase;
- String canonicalDocBase;
-
- try {
- String canonicalAppBase = appBase.getCanonicalPath();
- docBase = new StringBuilder(canonicalAppBase);
- if (canonicalAppBase.endsWith(File.separator)) {
- docBase.append(contextPath.substring(1).replace('/',
File.separatorChar));
- } else {
- docBase.append(contextPath.replace('/', File.separatorChar));
- }
- // At this point docBase should be canonical but will not end
- // with File.separator
-
- canonicalDocBase = (new
File(docBase.toString())).getCanonicalPath();
-
- // If the canonicalDocBase ends with File.separator, add one to
- // docBase before they are compared
- if (canonicalDocBase.endsWith(File.separator)) {
- docBase.append(File.separator);
- }
- } catch (IOException ioe) {
- return false;
- }
-
- // Compare the two. If they are not the same, the contextPath must
- // have /../ like sequences in it
- return canonicalDocBase.contentEquals(docBase);
- }
-
/**
* Deploy packed WAR.
* <p>
@@ -810,6 +785,13 @@ public class HostConfig implements LifecycleListener {
*/
protected void deployWAR(ContextName cn, File war) {
+ // Check for WARs with /../ /./ or similar sequences in the name
+ if (!cn.isPathValid()) {
+ log.error(sm.getString("hostConfig.illegalWarName",
war.getName()));
+ invalidWars.add(war.getName());
+ return;
+ }
+
File xml = new File(host.getAppBaseFile(), cn.getBaseName() + "/" +
Constants.ApplicationContextXml);
File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() +
Constants.WarTracker);
@@ -1003,7 +985,7 @@ public class HostConfig implements LifecycleListener {
}
File dir = new File(appBase, file);
- if (dir.isDirectory()) {
+ if (dir.isDirectory() && !invalidDirectories.contains(file)) {
ContextName cn = new ContextName(file, false);
if (tryAddServiced(cn.getName())) {
@@ -1044,6 +1026,13 @@ public class HostConfig implements LifecycleListener {
*/
protected void deployDirectory(ContextName cn, File dir) {
+ // Check for directories with /../ /./ or similar sequences in the name
+ if (!cn.isPathValid()) {
+ log.error(sm.getString("hostConfig.illegalDirName",
dir.getName()));
+ invalidDirectories.add(dir.getName());
+ return;
+ }
+
long startTime = 0;
// Deploy the application in this directory
if (log.isInfoEnabled()) {
diff --git a/java/org/apache/catalina/startup/LocalStrings.properties
b/java/org/apache/catalina/startup/LocalStrings.properties
index 37d133469d..088864e4e3 100644
--- a/java/org/apache/catalina/startup/LocalStrings.properties
+++ b/java/org/apache/catalina/startup/LocalStrings.properties
@@ -142,6 +142,8 @@ hostConfig.docBaseUrlInvalid=The provided docBase cannot be
expressed as a URL
hostConfig.expand=Expanding web application archive [{0}]
hostConfig.expand.error=Exception while expanding web application archive [{0}]
hostConfig.ignorePath=Ignoring path [{0}] in appBase for automatic deployment
+hostConfig.illegalDescriptorName=The descriptor name [{0}] is invalid. The
archive will be ignored.
+hostConfig.illegalDirName=The directory name [{0}] is invalid. The archive
will be ignored.
hostConfig.illegalWarName=The war name [{0}] is invalid. The archive will be
ignored.
hostConfig.jmx.register=Register context [{0}] failed
hostConfig.jmx.unregister=Unregister context [{0}] failed
diff --git a/java/org/apache/catalina/util/ContextName.java
b/java/org/apache/catalina/util/ContextName.java
index 618fc3cffe..2bb4cf1cd9 100644
--- a/java/org/apache/catalina/util/ContextName.java
+++ b/java/org/apache/catalina/util/ContextName.java
@@ -19,6 +19,8 @@ package org.apache.catalina.util;
import java.util.Locale;
import java.util.Objects;
+import org.apache.tomcat.util.http.RequestUtil;
+
/**
* Utility class to manage context names so there is one place where the
conversions between baseName, path and version
* take place.
@@ -196,6 +198,18 @@ public final class ContextName {
}
+ public boolean isPathValid() {
+ // No need to test for null since path can never be null (see
constructors)
+ //
+ // Therefore, just need to check:
+ // - empty or start with /
+ // - normalized and no attempt to escape the root
+ //
+ // Note: Normalize check on empty path would fail
+ return path.isEmpty() || (path.startsWith("/") &&
path.equals(RequestUtil.normalize(path)));
+ }
+
+
/**
* Extract the final component of the given path which is assumed to be a
base name and generate a
* {@link ContextName} from that base name.
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index a5b00a454b..4059918d82 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -338,6 +338,10 @@
Manager: Add checks to ensure that any uploaded files are uploaded to
the expected location. (markt)
</add>
+ <add>
+ Manager: Add checks to ensure that the requested context path for a
+ deployed WAR, directory or descriptor file is valid. (markt)
+ </add>
</changelog>
</subsection>
<subsection name="jdbc-pool">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]