Index: java/org/apache/tomcat/util/compat/JrePlatform.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/tomcat/util/compat/JrePlatform.java	(revision )
+++ java/org/apache/tomcat/util/compat/JrePlatform.java	(revision )
@@ -0,0 +1,59 @@
+/*
+ *  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.tomcat.util.compat;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+public class JrePlatform {
+
+    private static final String OS_NAME_PROPERTY = "os.name";
+    private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
+
+    static {
+        /*
+         * There are a few places where a) the behaviour of the Java API depends
+         * on the underlying platform and b) those behavioural differences have
+         * an impact on Tomcat.
+         *
+         * Tomcat therefore needs to be able to determine the platform it is
+         * running on to account for those differences.
+         *
+         * In an ideal world this code would not exist.
+         */
+
+        // This check is derived from the check in Apache Commons Lang
+        String osName;
+        if (System.getSecurityManager() == null) {
+            osName = System.getProperty(OS_NAME_PROPERTY);
+        } else {
+            osName = AccessController.doPrivileged(
+                    new PrivilegedAction<String>() {
+
+                    @Override
+                    public String run() {
+                        return System.getProperty(OS_NAME_PROPERTY);
+                    }
+                });
+        }
+
+        IS_WINDOWS = osName.startsWith(OS_NAME_WINDOWS_PREFIX);
+    }
+
+
+    public static final boolean IS_WINDOWS;
+}
Index: test/org/apache/catalina/webresources/TestAbstractFileResourceSetPerformance.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- test/org/apache/catalina/webresources/TestAbstractFileResourceSetPerformance.java	(revision )
+++ test/org/apache/catalina/webresources/TestAbstractFileResourceSetPerformance.java	(revision )
@@ -0,0 +1,90 @@
+/*
+ * 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.webresources;
+
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+
+public class TestAbstractFileResourceSetPerformance {
+
+    private static final Pattern UNSAFE_WINDOWS_FILENAME_PATTERN = Pattern.compile(" $|[\"<>]");
+
+    private static final int LOOPS = 10_000_000;
+
+    /*
+     * Checking individual characters is about 10 times faster on markt's dev
+     * PC for typical length file names. The file names need to get to ~65
+     * characters before the Pattern matching is faster.
+     */
+    @Test
+    public void testFileNameFiltering() {
+
+        long start = System.nanoTime();
+        for (int i = 0; i < LOOPS; i++) {
+            UNSAFE_WINDOWS_FILENAME_PATTERN.matcher("testfile.jsp ").find();
+        }
+        long end = System.nanoTime();
+        System.out.println("Regular expression took " + (end - start) + "ns or " +
+                (end-start)/LOOPS + "ns per iteration");
+
+        start = System.nanoTime();
+        for (int i = 0; i < LOOPS; i++) {
+            checkForBadCharsArray("testfile.jsp ");
+        }
+        end = System.nanoTime();
+        System.out.println("char[] check took " + (end - start) + "ns or " +
+                (end-start)/LOOPS + "ns per iteration");
+
+        start = System.nanoTime();
+        for (int i = 0; i < LOOPS; i++) {
+            checkForBadCharsAt("testfile.jsp ");
+        }
+        end = System.nanoTime();
+        System.out.println("charAt() check took " + (end - start) + "ns or " +
+                (end-start)/LOOPS + "ns per iteration");
+
+    }
+
+    private boolean checkForBadCharsArray(String filename) {
+        char[] chars = filename.toCharArray();
+        for (char c : chars) {
+            if (c == '\"' || c == '<' || c == '>') {
+                return false;
+            }
+        }
+        if (chars[chars.length -1] == ' ') {
+            return false;
+        }
+        return true;
+    }
+
+
+    private boolean checkForBadCharsAt(String filename) {
+        final int len = filename.length();
+        for (int i = 0; i < len; i++) {
+            char c = filename.charAt(i);
+            if (c == '\"' || c == '<' || c == '>') {
+                return false;
+            }
+        }
+        if (filename.charAt(len - 1) == ' ') {
+            return false;
+        }
+        return true;
+    }
+}
Index: java/org/apache/catalina/webresources/AbstractFileResourceSet.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- java/org/apache/catalina/webresources/AbstractFileResourceSet.java	(date 1490711901000)
+++ java/org/apache/catalina/webresources/AbstractFileResourceSet.java	(revision )
@@ -22,6 +22,7 @@
 import java.net.URL;
 
 import org.apache.catalina.LifecycleException;
+import org.apache.tomcat.util.compat.JrePlatform;
 import org.apache.tomcat.util.http.RequestUtil;
 
 public abstract class AbstractFileResourceSet extends AbstractResourceSet {
@@ -57,53 +58,113 @@
             name = "";
         }
         File file = new File(fileBase, name);
-        if (!mustExist || file.canRead()) {
 
-            if (getRoot().getAllowLinking()) {
-                return file;
-            }
+        // If the requested names ends in '/', the Java File API will return a
+        // matching file if one exists. This isn't what we want as it is not
+        // consistent with the Servlet spec rules for request mapping.
+        if (name.endsWith("/") && file.isFile()) {
+            return null;
+        }
+
+        // If the file/dir must exist but the identified file/dir can't be read
+        // then signal that the resource was not found
+        if (mustExist && !file.canRead()) {
+            return null;
+        }
+
+        // If allow linking is enabled, files are not limited to being located
+        // under the fileBase so all further checks are disabled.
+        if (getRoot().getAllowLinking()) {
+            return file;
+        }
 
-            // Check that this file is located under the WebResourceSet's base
-            String canPath = null;
-            try {
-                canPath = file.getCanonicalPath();
-            } catch (IOException e) {
-                // Ignore
-            }
-            if (canPath == null)
-                return null;
+        // Additional Windows specific checks to handle known problems with
+        // File.getCanonicalPath()
+        if (JrePlatform.IS_WINDOWS && isInvalidWindowsFilename(name)) {
+            return null;
+        }
+
+        // Check that this file is located under the WebResourceSet's base
+        String canPath = null;
+        try {
+            canPath = file.getCanonicalPath();
+        } catch (IOException e) {
+            // Ignore
+        }
+        if (canPath == null || !canPath.startsWith(canonicalBase)) {
+            return null;
+        }
 
-            if (!canPath.startsWith(canonicalBase)) {
-                return null;
-            }
+        // Ensure that the file is not outside the fileBase. This should not be
+        // possible for standard requests (the request is normalized early in
+        // the request processing) but might be possible for some access via the
+        // Servlet API (RequestDispatcher, HTTP/2 push etc.) therefore these
+        // checks are retained as an additional safety measure
+        // absoluteBase has been normalized so absPath needs to be normalized as
+        // well.
+        String absPath = normalize(file.getAbsolutePath());
+        if (absoluteBase.length() > absPath.length()) {
+            return null;
+        }
 
-            // Case sensitivity check
-            // Note: We know the resource is located somewhere under base at
-            //       point. The purpose of this code is to check in a case
-            //       sensitive manner, the path to the resource under base
-            //       agrees with what was requested
-            String fileAbsPath = file.getAbsolutePath();
-            if (fileAbsPath.endsWith("."))
-                fileAbsPath = fileAbsPath + '/';
-            String absPath = normalize(fileAbsPath);
-            if ((absoluteBase.length() < absPath.length())
-                && (canonicalBase.length() < canPath.length())) {
-                absPath = absPath.substring(absoluteBase.length() + 1);
-                if (absPath.equals(""))
-                    absPath = "/";
-                canPath = canPath.substring(canonicalBase.length() + 1);
-                if (canPath.equals(""))
-                    canPath = "/";
-                if (!canPath.equals(absPath))
-                    return null;
-            }
+        // Remove the fileBase location from the start of the paths since that
+        // was not part of the requested path and the remaining check only
+        // applies to the request path
+        absPath = absPath.substring(absoluteBase.length());
+        canPath = canPath.substring(canonicalBase.length());
+
+        // Case sensitivity check
+        // The normalized requested path should be an exact match the equivalent
+        // canonical path. If it is not, possible reasons include:
+        // - case differences on case insensitive file systems
+        // - Windows removing a trailing ' ' or '.' from the file name
+        //
+        // In all cases, a mis-match here results in the resource not being
+        // found
+        //
+        // absPath is normalized so canPath needs to be normalized as well
+        // Can't normalize canPath earlier as canonicalBase is not normalized
+        if (canPath.length() > 0) {
+            canPath = normalize(canPath);
+        }
+        if (!canPath.equals(absPath)) {
+            return null;
+        }
 
-        } else {
-            return null;
-        }
         return file;
     }
 
+
+    private boolean isInvalidWindowsFilename(String name) {
+        final int len = name.length();
+        if (len == 0) {
+            return false;
+        }
+        // This consistently ~10 times faster than the equivalent regular
+        // expression irrespective of input length.
+        for (int i = 0; i < len; i++) {
+            char c = name.charAt(i);
+            if (c == '\"' || c == '<' || c == '>') {
+                // These characters are disallowed in Windows file names and
+                // there are known problems for file names with these characters
+                // when using File#getCanonicalPath().
+                // Note: There are additional characters that are disallowed in
+                //       Windows file names but these are not known to cause
+                //       problems when using File#getCanonicalPath().
+                return true;
+            }
+        }
+        // Windows does not allow file names to end in ' ' unless specific low
+        // level APIs are used to create the files that bypass various checks.
+        // File names that end in ' ' are known to cause problems when using
+        // File#getCanonicalPath().
+        if (name.charAt(len -1) == ' ') {
+            return true;
+        }
+        return false;
+    }
+
+
     /**
      * Return a context-relative path, beginning with a "/", that represents
      * the canonical version of the specified path after ".." and "." elements
@@ -114,7 +175,7 @@
      * @param path Path to be normalized
      */
     private String normalize(String path) {
-        return RequestUtil.normalize(path, File.separatorChar == '/');
+        return RequestUtil.normalize(path, File.separatorChar == '\\');
     }
 
     @Override
@@ -144,11 +205,7 @@
         fileBase = new File(getBase(), getInternalPath());
         checkType(fileBase);
 
-        String absolutePath = fileBase.getAbsolutePath();
-        if (absolutePath.endsWith(".")) {
-            absolutePath = absolutePath + '/';
-        }
-        this.absoluteBase = normalize(absolutePath);
+        this.absoluteBase = normalize(fileBase.getAbsolutePath());
 
         try {
             this.canonicalBase = fileBase.getCanonicalPath();
