Description: Fixes CVE-2015-5345: The Mapper component in Apache Tomcat processes
 redirects before considering security constraints and Filters, which allows
 remote attackers to determine the existence of a directory via a URL that lacks
 a trailing / (slash) character.
Author: Markus Koschany <apo@debian.org>
Origin: backport, https://svn.apache.org/r1715207
                  https://svn.apache.org/r1717209
--- a/java/org/apache/catalina/Context.java
+++ b/java/org/apache/catalina/Context.java
@@ -1674,4 +1674,44 @@
      * processing cookies using the RFC6265 based cookie parser.
      */
     public Charset getCookieEncodingCharset();
+
+    /**
+     * If enabled, requests for a web application context root will be
+     * redirected (adding a trailing slash) by the Mapper. This is more
+     * efficient but has the side effect of confirming that the context path is
+     * valid.
+     *
+     * @param mapperContextRootRedirectEnabled Should the redirects be enabled?
+     */
+    public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled);
+
+    /**
+     * Determines if requests for a web application context root will be
+     * redirected (adding a trailing slash) by the Mapper. This is more
+     * efficient but has the side effect of confirming that the context path is
+     * valid.
+     *
+     * @return {@code true} if the Mapper level redirect is enabled for this
+     *         Context.
+     */
+    public boolean getMapperContextRootRedirectEnabled();
+
+    /**
+     * If enabled, requests for a directory will be redirected (adding a
+     * trailing slash) by the Mapper. This is more efficient but has the
+     * side effect of confirming that the directory is valid.
+     *
+     * @param mapperDirectoryRedirectEnabled Should the redirects be enabled?
+     */
+    public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled);
+
+    /**
+     * Determines if requests for a directory will be redirected (adding a
+     * trailing slash) by the Mapper. This is more efficient but has the
+     * side effect of confirming that the directory is valid.
+     *
+     * @return {@code true} if the Mapper level redirect is enabled for this
+     *         Context.
+     */
+    public boolean getMapperDirectoryRedirectEnabled();
 }
--- a/java/org/apache/catalina/authenticator/FormAuthenticator.java
+++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java
@@ -241,6 +241,20 @@
 
         // No -- Save this request and redirect to the form login page
         if (!loginAction) {
+            // If this request was to the root of the context without a trailing
+            // '/', need to redirect to add it else the submit of the login form
+            // may not go to the correct web application
+            if (request.getServletPath().length() == 0 && request.getPathInfo() == null) {
+                StringBuilder location = new StringBuilder(requestURI);
+                location.append('/');
+                if (request.getQueryString() != null) {
+                    location.append('?');
+                    location.append(request.getQueryString());
+                }
+                response.sendRedirect(response.encodeRedirectURL(location.toString()));
+                return false;
+            }
+
             session = request.getSessionInternal(true);
             if (log.isDebugEnabled()) {
                 log.debug("Save request in session '" + session.getIdInternal() + "'");
--- a/java/org/apache/catalina/core/StandardContext.java
+++ b/java/org/apache/catalina/core/StandardContext.java
@@ -828,9 +828,44 @@
     private boolean useRfc6265 = false;
     private Charset cookieEncoding = StandardCharsets.UTF_8;
 
+    boolean mapperContextRootRedirectEnabled = false;
+    boolean mapperDirectoryRedirectEnabled = false;
 
     // ----------------------------------------------------- Context Properties
 
+    @Override
+    public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
+        this.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default value for this implementation is {@code false}.
+     */
+    @Override
+    public boolean getMapperContextRootRedirectEnabled() {
+        return mapperContextRootRedirectEnabled;
+    }
+
+
+    @Override
+    public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
+        this.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default value for this implementation is {@code false}.
+     */
+    @Override
+    public boolean getMapperDirectoryRedirectEnabled() {
+        return mapperDirectoryRedirectEnabled;
+    }
+
 
     @Override
     public void setUseRfc6265(boolean useRfc6265) {
--- a/java/org/apache/catalina/core/mbeans-descriptors.xml
+++ b/java/org/apache/catalina/core/mbeans-descriptors.xml
@@ -181,6 +181,14 @@
                description="Associated manager."
                type="org.apache.catalina.Manager" />
 
+    <attribute name="mapperContextRootRedirectEnabled"
+               description="Should the Mapper be used for context root redirects"
+               type="boolean" />
+
+    <attribute name="mapperDirectoryRedirectEnabled"
+               description="Should the Mapper be used for directory redirects"
+               type="boolean" />
+
     <attribute name="namingContextListener"
                description="Associated naming context listener."
                type="org.apache.catalina.core.NamingContextListener" />
--- a/java/org/apache/catalina/mapper/Mapper.java
+++ b/java/org/apache/catalina/mapper/Mapper.java
@@ -830,20 +830,13 @@
 
         int pathOffset = path.getOffset();
         int pathEnd = path.getEnd();
-        int servletPath = pathOffset;
         boolean noServletPath = false;
 
         int length = contextVersion.path.length();
-        if (length != (pathEnd - pathOffset)) {
-            servletPath = pathOffset + length;
-        } else {
+        if (length == (pathEnd - pathOffset)) {
             noServletPath = true;
-            path.append('/');
-            pathOffset = path.getOffset();
-            pathEnd = path.getEnd();
-            servletPath = pathOffset+length;
         }
-
+        int servletPath = pathOffset + length;
         path.setOffset(servletPath);
 
         // Rule 1 -- Exact Match
@@ -878,8 +871,11 @@
             }
         }
 
-        if(mappingData.wrapper == null && noServletPath) {
+        if(mappingData.wrapper == null && noServletPath &&
+                mappingData.context.getMapperContextRootRedirectEnabled()) {
             // The path is empty, redirect to "/"
+            path.append('/');
+            pathEnd = path.getEnd();
             mappingData.redirectPath.setChars
                 (path.getBuffer(), pathOffset, pathEnd-pathOffset);
             path.setEnd(pathEnd - 1);
@@ -996,9 +992,15 @@
             char[] buf = path.getBuffer();
             if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
                 String pathStr = path.toString();
-                WebResource file =
-                        contextVersion.resources.getResource(pathStr);
-                if (file != null && file.isDirectory()) {
+                WebResource file;
+                // Handle context root
+                if (pathStr.length() == 0) {
+                    file = contextVersion.resources.getResource("/");
+                } else {
+                    file = contextVersion.resources.getResource(pathStr);
+                }
+                if (file != null && file.isDirectory() &&
+                        mappingData.context.getMapperDirectoryRedirectEnabled()) {
                     // Note: this mutates the path: do not do any processing
                     // after this (since we set the redirectPath, there
                     // shouldn't be any)
--- a/java/org/apache/catalina/servlets/DefaultServlet.java
+++ b/java/org/apache/catalina/servlets/DefaultServlet.java
@@ -342,6 +342,10 @@
      * @param request The servlet request we are processing
      */
     protected String getRelativePath(HttpServletRequest request) {
+        return getRelativePath(request, false);
+    }
+
+    protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
         // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
         // serves resources from the web app root with context rooted paths.
         // i.e. it can not be used to mount the web app root under a sub-path
@@ -703,7 +707,8 @@
         boolean serveContent = content;
 
         // Identify the requested resource path
-        String path = getRelativePath(request);
+        String path = getRelativePath(request, true);
+
         if (debug > 0) {
             if (serveContent)
                 log("DefaultServlet.serveResource:  Serving resource '" +
@@ -713,6 +718,12 @@
                     path + "' headers only");
         }
 
+        if (path.length() == 0) {
+            // Context root redirect
+            doDirectoryRedirect(request, response);
+            return;
+        }
+
         WebResource resource = resources.getResource(path);
 
         if (!resource.exists()) {
@@ -827,6 +838,11 @@
         long contentLength = -1L;
 
         if (resource.isDirectory()) {
+            if (!path.endsWith("/")) {
+                doDirectoryRedirect(request, response);
+                return;
+            }
+
             // Skip directory listings if we have been configured to
             // suppress them
             if (!listings) {
@@ -1032,6 +1048,16 @@
         }
     }
 
+    private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        StringBuilder location = new StringBuilder(request.getRequestURI());
+        location.append('/');
+        if (request.getQueryString() != null) {
+            location.append('?');
+            location.append(request.getQueryString());
+        }
+        response.sendRedirect(response.encodeRedirectURL(location.toString()));
+    }
 
     /**
      * Parse the content-range header.
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -375,6 +375,11 @@
      */
     @Override
     protected String getRelativePath(HttpServletRequest request) {
+        return getRelativePath(request, false);
+    }
+
+    @Override
+    protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
         // Are we being processed by a RequestDispatcher.include()?
         if (request.getAttribute(
                 RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
--- a/java/org/apache/catalina/startup/FailedContext.java
+++ b/java/org/apache/catalina/startup/FailedContext.java
@@ -771,4 +771,21 @@
 
     @Override
     public Charset getCookieEncodingCharset() { return StandardCharsets.UTF_8; }
-}
\ No newline at end of file
+
+    @Override
+    public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
+        // NO-OP
+    }
+
+    @Override
+    public boolean getMapperContextRootRedirectEnabled() { return false; }
+
+    @Override
+    public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
+        // NO-OP
+    }
+
+    @Override
+    public boolean getMapperDirectoryRedirectEnabled() { return false; }
+
+}
--- a/test/org/apache/catalina/core/TesterContext.java
+++ b/test/org/apache/catalina/core/TesterContext.java
@@ -1238,4 +1238,21 @@
 
     @Override
     public Charset getCookieEncodingCharset() { return StandardCharsets.UTF_8; }
+
+    @Override
+    public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
+        // NO-OP
+    }
+
+    @Override
+    public boolean getMapperContextRootRedirectEnabled() { return false; }
+
+    @Override
+    public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
+        // NO-OP
+    }
+
+    @Override
+    public boolean getMapperDirectoryRedirectEnabled() { return false; }
+
 }
--- a/test/org/apache/catalina/mapper/TestMapperWebapps.java
+++ b/test/org/apache/catalina/mapper/TestMapperWebapps.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.net.HttpURLConnection;
 import java.util.HashMap;
 import java.util.List;
 
@@ -33,7 +34,10 @@
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.catalina.valves.RemoteAddrValve;
 import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
 import org.apache.tomcat.websocket.server.WsContextListener;
 
 /**
@@ -226,6 +230,66 @@
         Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc);
     }
 
+    @Test
+    public void testRedirect() throws Exception {
+        // Disable the following of redirects for this test only
+        boolean originalValue = HttpURLConnection.getFollowRedirects();
+        HttpURLConnection.setFollowRedirects(false);
+        try {
+            Tomcat tomcat = getTomcatInstance();
+
+            // Use standard test webapp as ROOT
+            File rootDir = new File("test/webapp");
+            org.apache.catalina.Context root =
+                    tomcat.addWebapp(null, "", rootDir.getAbsolutePath());
+
+            // Add a security constraint
+            SecurityConstraint constraint = new SecurityConstraint();
+            SecurityCollection collection = new SecurityCollection();
+            collection.addPattern("/welcome-files/*");
+            collection.addPattern("/welcome-files");
+            constraint.addCollection(collection);
+            constraint.addAuthRole("foo");
+            root.addConstraint(constraint);
+
+            // Also make examples available
+            File examplesDir = new File(getBuildDirectory(), "webapps/examples");
+            org.apache.catalina.Context examples  = tomcat.addWebapp(
+                    null, "/examples", examplesDir.getAbsolutePath());
+            // Then block access to the examples to test redirection
+            RemoteAddrValve rav = new RemoteAddrValve();
+            rav.setDeny(".*");
+            rav.setDenyStatus(404);
+            examples.getPipeline().addValve(rav);
+
+            tomcat.start();
+
+            // Redirects within a web application
+            doRedirectTest("/welcome-files", 401);
+            doRedirectTest("/welcome-files/", 401);
+
+            doRedirectTest("/jsp", 302);
+            doRedirectTest("/jsp/", 404);
+
+            doRedirectTest("/WEB-INF", 404);
+            doRedirectTest("/WEB-INF/", 404);
+
+            // Redirects between web applications
+            doRedirectTest("/examples", 404);
+            doRedirectTest("/examples/", 404);
+        } finally {
+            HttpURLConnection.setFollowRedirects(originalValue);
+        }
+    }
+
+
+    private void doRedirectTest(String path, int expected) throws IOException {
+        ByteChunk bc = new ByteChunk();
+        int rc = getUrl("http://localhost:" + getPort() + path, bc, null);
+        Assert.assertEquals(expected, rc);
+    }
+
+
     /**
      * Prepare a string to search in messages that contain a timestamp, when it
      * is known that the timestamp was printed between {@code timeA} and
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -184,6 +184,16 @@
         Reduce duplicated code. All AJP connectors use common method to
         configuration of processor. (kfujino)
       </fix>
+      <add>
+        Move the functionality that provides redirects for context roots and
+        directories where a trailing <code>/</code> is added from the Mapper to
+        the <code>DefaultServlet</code>. This enables such requests to be
+        processed by any configured Valves and Filters before the redirect is
+        made. This behaviour is configurable via the
+        <code>mapperContextRootRedirectEnabled</code> and
+        <code>mapperDirectoryRedirectEnabled</code> attributes of the Context
+        which may be used to restore the previous behaviour. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">
@@ -275,6 +285,11 @@
         leak fixes and support for application provided eviction policies.
         (markt)
       </fix>
+      <fix>
+        <bug>58660</bug>: Correct a regression in 8.0.29 caused by the change
+        that moved the redirection for context roots from the Mapper to the
+        Default Servlet. (markt)
+      </fix>
     </changelog>
   </subsection>
 </section>
--- a/webapps/docs/config/context.xml
+++ b/webapps/docs/config/context.xml
@@ -367,6 +367,22 @@
         default value of <code>false</code> is used.</p>
       </attribute>
 
+      <attribute name="mapperContextRootRedirectEnabled" required="false">
+        <p>If enabled, requests for a web application context root will be
+        redirected (adding a trailing slash) if necessary by the Mapper rather
+        than the default Servlet. This is more efficient but has the side effect
+        of confirming that the context path exists. If not specified, the
+        default value of <code>false</code> is used.</p>
+      </attribute>
+
+      <attribute name="mapperDirectoryRedirectEnabled" required="false">
+        <p>If enabled, requests for a web application directory will be
+        redirected (adding a trailing slash) if necessary by the Mapper rather
+        than the default Servlet. This is more efficient but has the side effect
+        of confirming that the directory is exists. If not specified, the
+        default value of <code>false</code> is used.</p>
+      </attribute>
+
       <attribute name="override" required="false">
         <p>Set to <code>true</code> to ignore any settings in both the global
         or <a href="host.html">Host</a> default contexts.  By default, settings
