You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2022/02/15 16:50:14 UTC

[tomcat] branch main updated: Improve validation of Destination header

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 aea0e1c  Improve validation of Destination header
aea0e1c is described below

commit aea0e1ce1a49eae90f91b49b8fddcfa08678b14d
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Feb 15 16:50:01 2022 +0000

    Improve validation of Destination header
---
 .../apache/catalina/servlets/WebdavServlet.java    | 113 +++++++++++----------
 webapps/docs/changelog.xml                         |   4 +
 2 files changed, 61 insertions(+), 56 deletions(-)

diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java
index c6a0963..b67db65 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -22,6 +22,8 @@ import java.io.Serializable;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.util.Date;
 import java.util.Enumeration;
@@ -47,7 +49,6 @@ import org.apache.catalina.connector.RequestFacade;
 import org.apache.catalina.util.DOMWriter;
 import org.apache.catalina.util.URLEncoder;
 import org.apache.catalina.util.XMLWriter;
-import org.apache.tomcat.util.buf.UDecoder;
 import org.apache.tomcat.util.http.ConcurrentDateFormat;
 import org.apache.tomcat.util.http.FastHttpDateFormat;
 import org.apache.tomcat.util.http.RequestUtil;
@@ -1541,76 +1542,76 @@ public class WebdavServlet extends DefaultServlet {
      *
      * @param req Servlet request
      * @param resp Servlet response
+     *
      * @return boolean true if the copy is successful
+     *
      * @throws IOException If an IO error occurs
      */
-    private boolean copyResource(HttpServletRequest req,
-                                 HttpServletResponse resp)
-            throws IOException {
+    private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+
+        // Check the source exists
+        String path = getRelativePath(req);
+        WebResource source = resources.getResource(path);
+        if (!source.exists()) {
+            resp.sendError(WebdavStatus.SC_NOT_FOUND);
+            return false;
+        }
 
         // Parsing destination header
+        // See RFC 4918
+        String destinationHeader = req.getHeader("Destination");
 
-        String destinationPath = req.getHeader("Destination");
+        if (destinationHeader == null  || destinationHeader.isEmpty()) {
+            resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+            return false;
+        }
 
-        if (destinationPath == null) {
+        URI destinationUri;
+        try {
+            destinationUri = new URI (destinationHeader);
+        } catch (URISyntaxException e) {
             resp.sendError(WebdavStatus.SC_BAD_REQUEST);
             return false;
         }
 
-        // Remove url encoding from destination
-        destinationPath = UDecoder.URLDecode(destinationPath, StandardCharsets.UTF_8);
+        String destinationPath = destinationUri.getPath();
 
-        int protocolIndex = destinationPath.indexOf("://");
-        if (protocolIndex >= 0) {
-            // if the Destination URL contains the protocol, we can safely
-            // trim everything upto the first "/" character after "://"
-            int firstSeparator =
-                destinationPath.indexOf('/', protocolIndex + 4);
-            if (firstSeparator < 0) {
-                destinationPath = "/";
-            } else {
-                destinationPath = destinationPath.substring(firstSeparator);
-            }
-        } else {
-            String hostName = req.getServerName();
-            if ((hostName != null) && (destinationPath.startsWith(hostName))) {
-                destinationPath = destinationPath.substring(hostName.length());
-            }
+        // Destination isn't allowed to use '.' or '..' segments
+        if (!destinationPath.equals(RequestUtil.normalize(destinationPath))) {
+            resp.sendError(WebdavStatus.SC_BAD_REQUEST);
+            return false;
+        }
 
-            int portIndex = destinationPath.indexOf(':');
-            if (portIndex >= 0) {
-                destinationPath = destinationPath.substring(portIndex);
+        if (destinationUri.isAbsolute()) {
+            // Scheme and host need to match
+            if (!req.getScheme().equals(destinationUri.getScheme()) ||
+                    !req.getServerName().equals(destinationUri.getHost())) {
+                resp.sendError(WebdavStatus.SC_FORBIDDEN);
+                return false;
             }
-
-            if (destinationPath.startsWith(":")) {
-                int firstSeparator = destinationPath.indexOf('/');
-                if (firstSeparator < 0) {
-                    destinationPath = "/";
+            // Port needs to match too but handled separately as the logic is a
+            // little more complicated
+            if (req.getServerPort() != destinationUri.getPort()) {
+                if (destinationUri.getPort() == -1 &&
+                        ("http".equals(req.getScheme()) && req.getServerPort() == 80 ||
+                        "https".equals(req.getScheme()) && req.getServerPort() == 443)) {
+                    // All good.
                 } else {
-                    destinationPath =
-                        destinationPath.substring(firstSeparator);
+                    resp.sendError(WebdavStatus.SC_FORBIDDEN);
+                    return false;
                 }
             }
         }
 
-        // Normalise destination path (remove '.' and '..')
-        destinationPath = RequestUtil.normalize(destinationPath);
-
-        String contextPath = req.getContextPath();
-        if ((contextPath != null) &&
-            (destinationPath.startsWith(contextPath))) {
-            destinationPath = destinationPath.substring(contextPath.length());
+        // Cross-context operations aren't supported
+        String reqContextPath = req.getContextPath();
+        if (!destinationPath.startsWith(reqContextPath + "/")) {
+            resp.sendError(WebdavStatus.SC_FORBIDDEN);
+            return false;
         }
 
-        String pathInfo = req.getPathInfo();
-        if (pathInfo != null) {
-            String servletPath = req.getServletPath();
-            if ((servletPath != null) &&
-                (destinationPath.startsWith(servletPath))) {
-                destinationPath = destinationPath
-                    .substring(servletPath.length());
-            }
-        }
+        // Remove context path & servlet path
+        destinationPath = destinationPath.substring(reqContextPath.length() + req.getServletPath().length());
 
         if (debug > 0) {
             log("Dest path :" + destinationPath);
@@ -1622,18 +1623,20 @@ public class WebdavServlet extends DefaultServlet {
             return false;
         }
 
-        String path = getRelativePath(req);
-
         if (destinationPath.equals(path)) {
             resp.sendError(WebdavStatus.SC_FORBIDDEN);
             return false;
         }
 
-        // Parsing overwrite header
+        // Check src / dest are not sub-dirs of each other
+        if (destinationPath.startsWith(path) && destinationPath.charAt(path.length()) == '/' ||
+                path.startsWith(destinationPath) && path.charAt(destinationPath.length()) == '/') {
+            resp.sendError(WebdavStatus.SC_FORBIDDEN);
+            return false;
+        }
 
         boolean overwrite = true;
         String overwriteHeader = req.getHeader("Overwrite");
-
         if (overwriteHeader != null) {
             if (overwriteHeader.equalsIgnoreCase("T")) {
                 overwrite = true;
@@ -1643,9 +1646,7 @@ public class WebdavServlet extends DefaultServlet {
         }
 
         // Overwriting the destination
-
         WebResource destination = resources.getResource(destinationPath);
-
         if (overwrite) {
             // Delete destination resource, if it exists
             if (destination.exists()) {
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 78d6345..54a0e15 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -150,6 +150,10 @@
         humans to read. The change ensures that there is always a line break
         before starting a new element. (markt)
       </fix>
+      <fix>
+        Improve validation of the <code>Destination</code> header for WebDAV
+        <code>MOVE</code> and <code>COPY</code> requests. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org