--- java/org/apache/catalina/servlets/DefaultServlet.java.orig 2017-10-13 09:41:05.734302404 -0400 +++ java/org/apache/catalina/servlets/DefaultServlet.java 2017-10-13 09:42:53.515701311 -0400 @@ -855,23 +855,6 @@ return; } - // If the resource is not a collection, and the resource path - // ends with "/" or "\", return NOT FOUND - if (cacheEntry.context == null) { - if (path.endsWith("/") || (path.endsWith("\\"))) { - // Check if we're included so we can return the appropriate - // missing resource name in the error - String requestUri = (String) request.getAttribute( - RequestDispatcher.INCLUDE_REQUEST_URI); - if (requestUri == null) { - requestUri = request.getRequestURI(); - } - response.sendError(HttpServletResponse.SC_NOT_FOUND, - requestUri); - return; - } - } - boolean isError = DispatcherType.ERROR == request.getDispatcherType(); // Check if the conditions specified in the optional If headers are --- java/org/apache/naming/resources/FileDirContext.java.orig 2017-10-13 09:41:05.737302387 -0400 +++ java/org/apache/naming/resources/FileDirContext.java 2017-10-13 09:42:53.516701306 -0400 @@ -14,8 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.naming.resources; import java.io.File; @@ -75,6 +73,8 @@ /** * Builds a file directory context using the given environment. + * + * @param env The environment with which to build the context */ public FileDirContext(Hashtable env) { super(env); @@ -95,6 +95,8 @@ */ protected String absoluteBase = null; + private String canonicalBase = null; + /** * Allow linking. @@ -104,7 +106,6 @@ // ------------------------------------------------------------- Properties - /** * Set the document root. * @@ -117,32 +118,41 @@ */ @Override public void setDocBase(String docBase) { + // Validate the format of the proposed document root + if (docBase == null) { + throw new IllegalArgumentException(sm.getString("resources.null")); + } - // Validate the format of the proposed document root - if (docBase == null) - throw new IllegalArgumentException - (sm.getString("resources.null")); - - // Calculate a File object referencing this document base directory - base = new File(docBase); + // Calculate a File object referencing this document base directory + base = new File(docBase); try { base = base.getCanonicalFile(); } catch (IOException e) { // Ignore } - // Validate that the document base is an existing directory - if (!base.exists() || !base.isDirectory() || !base.canRead()) - throw new IllegalArgumentException - (sm.getString("fileResources.base", docBase)); - this.absoluteBase = base.getAbsolutePath(); - super.setDocBase(docBase); + // Validate that the document base is an existing directory + if (!base.exists() || !base.isDirectory() || !base.canRead()) { + throw new IllegalArgumentException(sm.getString("fileResources.base", docBase)); + } + this.absoluteBase = normalize(base.getAbsolutePath()); + + // absoluteBase also needs to be normalized. Using the canonical path is + // the simplest way of doing this. + try { + this.canonicalBase = base.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + super.setDocBase(docBase); } /** * Set allow linking. + * + * @param allowLinking The new value for the attribute */ public void setAllowLinking(boolean allowLinking) { this.allowLinking = allowLinking; @@ -151,6 +161,8 @@ /** * Is linking allowed. + * + * @return {@code true} is linking is allowed, otherwise {@false} */ public boolean getAllowLinking() { return allowLinking; @@ -193,7 +205,7 @@ @Override protected Object doLookup(String name) { Object result = null; - File file = file(name); + File file = file(name, true); if (file == null) return null; @@ -230,7 +242,7 @@ public void unbind(String name) throws NamingException { - File file = file(name); + File file = file(name, true); if (file == null) throw new NameNotFoundException( @@ -255,22 +267,22 @@ * @exception NamingException if a naming exception is encountered */ @Override - public void rename(String oldName, String newName) - throws NamingException { + public void rename(String oldName, String newName) throws NamingException { - File file = file(oldName); + File file = file(oldName, true); - if (file == null) - throw new NameNotFoundException - (sm.getString("resources.notFound", oldName)); + if (file == null) { + throw new NameNotFoundException(sm.getString("resources.notFound", oldName)); + } - File newFile = new File(base, newName); + File newFile = file(newName, false); + if (newFile == null) { + throw new NamingException(sm.getString("resources.renameFail", oldName, newName)); + } if (!file.renameTo(newFile)) { - throw new NamingException(sm.getString("resources.renameFail", - oldName, newName)); + throw new NamingException(sm.getString("resources.renameFail", oldName, newName)); } - } @@ -291,11 +303,11 @@ protected List doListBindings(String name) throws NamingException { - File file = file(name); + File file = file(name, true); if (file == null) return null; - + return list(file); } @@ -395,7 +407,7 @@ throws NamingException { // Building attribute list - File file = file(name); + File file = file(name, true); if (file == null) return null; @@ -463,12 +475,20 @@ * @exception NamingException if a naming exception is encountered */ @Override - public void bind(String name, Object obj, Attributes attrs) - throws NamingException { + public void bind(String name, Object obj, Attributes attrs) throws NamingException { // Note: No custom attributes allowed - File file = new File(base, name); + // bind() is meant to create a file so ensure that the path doesn't end + // in '/' + if (name.endsWith("/")) { + throw new NamingException(sm.getString("resources.bindFailed", name)); + } + + File file = file(name, false); + if (file == null) { + throw new NamingException(sm.getString("resources.bindFailed", name)); + } if (file.exists()) throw new NameAlreadyBoundException (sm.getString("resources.alreadyBound", name)); @@ -503,7 +523,10 @@ // Note: No custom attributes allowed // Check obj type - File file = new File(base, name); + File file = file(name, false); + if (file == null) { + throw new NamingException(sm.getString("resources.bindFailed", name)); + } InputStream is = null; if (obj instanceof Resource) { @@ -583,13 +606,14 @@ public DirContext createSubcontext(String name, Attributes attrs) throws NamingException { - File file = new File(base, name); + File file = file(name, false); + if (file == null) { + throw new NamingException(sm.getString("resources.bindFailed", name)); + } if (file.exists()) - throw new NameAlreadyBoundException - (sm.getString("resources.alreadyBound", name)); + throw new NameAlreadyBoundException(sm.getString("resources.alreadyBound", name)); if (!file.mkdir()) - throw new NamingException - (sm.getString("resources.bindFailed", name)); + throw new NamingException(sm.getString("resources.bindFailed", name)); return (DirContext) lookup(name); } @@ -758,6 +782,7 @@ } + /** * Return a File object representing the specified normalized * context-relative path if it exists and is readable. Otherwise, @@ -766,51 +791,133 @@ * @param name Normalized context-relative path (with leading '/') */ protected File file(String name) { + return file(name, true); + } + + + /** + * Return a File object representing the specified normalized + * context-relative path if it exists and is readable. Otherwise, + * return null. + * + * @param name Normalized context-relative path (with leading '/') + * @param mustExist Must the specified resource exist? + */ + protected File file(String name, boolean mustExist) { + if (name.equals("/")) { + name = ""; + } File file = new File(base, name); - if (file.exists() && file.canRead()) { + return validate(file, name, mustExist, absoluteBase, canonicalBase); + } - if (allowLinking) - return file; - - // Check that this file belongs to our root path - String canPath = null; - try { - canPath = file.getCanonicalPath(); - } catch (IOException e) { - // Ignore - } - if (canPath == null) - return null; - // Check to see if going outside of the web application root - if (!canPath.startsWith(absoluteBase)) { - return null; - } + protected File validate(File file, String name, boolean mustExist, String absoluteBase, + String canonicalBase) { - // Case sensitivity check - this is now always done - String fileAbsPath = file.getAbsolutePath(); - if (fileAbsPath.endsWith(".")) - fileAbsPath = fileAbsPath + "/"; - String absPath = normalize(fileAbsPath); - canPath = normalize(canPath); - if ((absoluteBase.length() < absPath.length()) - && (absoluteBase.length() < canPath.length())) { - absPath = absPath.substring(absoluteBase.length() + 1); - if (absPath.equals("")) - absPath = "/"; - canPath = canPath.substring(absoluteBase.length() + 1); - if (canPath.equals("")) - canPath = "/"; - if (!canPath.equals(absPath)) - return null; - } + // 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; + } - } else { + // 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 (allowLinking) { + return file; + } + + // 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 web application root + String canPath = null; + try { + canPath = file.getCanonicalPath(); + } catch (IOException e) { + // Ignore + } + if (canPath == null || !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 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; } + + // 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; + } + 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; } @@ -1054,10 +1161,10 @@ return super.getResourceType(); } - + /** * Get canonical path. - * + * * @return String the file's canonical path */ @Override @@ -1071,10 +1178,6 @@ } return canonicalPath; } - - } - - } --- java/org/apache/naming/resources/VirtualDirContext.java.orig 2017-10-13 09:41:05.740302370 -0400 +++ java/org/apache/naming/resources/VirtualDirContext.java 2017-10-13 09:42:53.517701300 -0400 @@ -76,7 +76,8 @@ * be listed twice. *

* - * @param path + * @param path The set of file system paths and virtual paths to map them to + * in the required format */ public void setExtraResourcePaths(String path) { extraResourcePaths = path; @@ -106,13 +107,13 @@ } path = resSpec.substring(0, idx); } - String dir = resSpec.substring(idx + 1); + File dir = new File(resSpec.substring(idx + 1)); List resourcePaths = mappedResourcePaths.get(path); if (resourcePaths == null) { resourcePaths = new ArrayList(); mappedResourcePaths.put(path, resourcePaths); } - resourcePaths.add(dir); + resourcePaths.add(dir.getAbsolutePath()); } } if (mappedResourcePaths.isEmpty()) { @@ -151,15 +152,17 @@ String resourcesDir = dirList.get(0); if (name.equals(path)) { File f = new File(resourcesDir); - if (f.exists() && f.canRead()) { + f = validate(f, name, true, resourcesDir); + if (f != null) { return new FileResourceAttributes(f); } } path += "/"; if (name.startsWith(path)) { String res = name.substring(path.length()); - File f = new File(resourcesDir + "/" + res); - if (f.exists() && f.canRead()) { + File f = new File(resourcesDir, res); + f = validate(f, res, true, resourcesDir); + if (f != null) { return new FileResourceAttributes(f); } } @@ -168,9 +171,16 @@ throw initialException; } + @Override protected File file(String name) { - File file = super.file(name); + return file(name, true); + } + + + @Override + protected File file(String name, boolean mustExist) { + File file = super.file(name, true); if (file != null || mappedResourcePaths == null) { return file; } @@ -185,7 +195,8 @@ if (name.equals(path)) { for (String resourcesDir : dirList) { file = new File(resourcesDir); - if (file.exists() && file.canRead()) { + file = validate(file, name, true, resourcesDir); + if (file != null) { return file; } } @@ -194,7 +205,8 @@ String res = name.substring(path.length()); for (String resourcesDir : dirList) { file = new File(resourcesDir, res); - if (file.exists() && file.canRead()) { + file = validate(file, res, true, resourcesDir); + if (file != null) { return file; } } @@ -229,7 +241,8 @@ if (res != null) { for (String resourcesDir : dirList) { File f = new File(resourcesDir, res); - if (f.exists() && f.canRead() && f.isDirectory()) { + f = validate(f, res, true, resourcesDir); + if (f != null && f.isDirectory()) { List virtEntries = super.list(f); for (NamingEntry entry : virtEntries) { // filter duplicate @@ -264,7 +277,8 @@ if (name.equals(path)) { for (String resourcesDir : dirList) { File f = new File(resourcesDir); - if (f.exists() && f.canRead()) { + f = validate(f, name, true, resourcesDir); + if (f != null) { if (f.isFile()) { return new FileResource(f); } @@ -279,8 +293,9 @@ if (name.startsWith(path)) { String res = name.substring(path.length()); for (String resourcesDir : dirList) { - File f = new File(resourcesDir + "/" + res); - if (f.exists() && f.canRead()) { + File f = new File(resourcesDir, res); + f = validate(f, res, true, resourcesDir); + if (f != null) { if (f.isFile()) { return new FileResource(f); } @@ -304,4 +319,9 @@ return null; } } + + + protected File validate(File file, String name, boolean mustExist, String absoluteBase) { + return validate(file, name, mustExist, normalize(absoluteBase), absoluteBase); + } } --- webapps/docs/changelog.xml.orig 2017-10-13 09:15:35.996884086 -0400 +++ webapps/docs/changelog.xml 2017-10-13 09:44:50.895046977 -0400 @@ -64,6 +64,14 @@ 61101: CORS filter should set Vary header in response. Submitted by Rick Riemer. (remm) + + Correct regression in 7.0.80 that broke WebDAV. (markt) + + + 61542: Fix CVE-2017-12617 and prevent JSPs from being + uploaded via a specially crafted request when HTTP PUT was enabled. + (markt) + --- java/org/apache/naming/resources/JrePlatform.java.orig 2017-10-13 09:41:05.745302342 -0400 +++ java/org/apache/naming/resources/JrePlatform.java 2017-10-13 09:42:53.516701306 -0400 @@ -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.naming.resources; + +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() { + + @Override + public String run() { + return System.getProperty(OS_NAME_PROPERTY); + } + }); + } + + IS_WINDOWS = osName.startsWith(OS_NAME_WINDOWS_PREFIX); + } + + + public static final boolean IS_WINDOWS; +} --- test/org/apache/naming/resources/TestFileDirContext.java.orig 2017-10-13 09:45:35.991795584 -0400 +++ test/org/apache/naming/resources/TestFileDirContext.java 2017-10-13 09:42:53.517701300 -0400 @@ -0,0 +1,46 @@ +/* + * 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.naming.resources; + +import java.io.File; + +import javax.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestFileDirContext extends TomcatBaseTest { + + @Test + public void testLookupResourceWithTrailingSlash() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp-3.0"); + // app dir is relative to server home + tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + tomcat.start(); + + int sc = getUrl("http://localhost:" + getPort() + + "/test/index.html/", new ByteChunk(), null); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, sc); + } +}