diff --git a/CHANGES.rst b/CHANGES.rst index be9af7f85..42bd46af0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,8 @@ Unreleased reloader to fail. :issue:`1607` - Work around an issue where the reloader couldn't introspect a setuptools script installed as an egg. :issue:`1600` +- ``SharedDataMiddleware`` safely handles paths with Windows drive + names. :issue:`1589` Version 0.15.4 diff --git a/src/werkzeug/middleware/shared_data.py b/src/werkzeug/middleware/shared_data.py index a902281da..088504a92 100644 --- a/src/werkzeug/middleware/shared_data.py +++ b/src/werkzeug/middleware/shared_data.py @@ -22,6 +22,7 @@ from ..filesystem import get_filesystem_encoding from ..http import http_date from ..http import is_resource_modified +from ..security import safe_join from ..wsgi import get_path_info from ..wsgi import wrap_file @@ -149,7 +150,7 @@ def loader(path): if path is None: return None, None - path = posixpath.join(package_path, path) + path = safe_join(package_path, path) if not provider.has_resource(path): return None, None @@ -170,7 +171,7 @@ def loader(path): def get_directory_loader(self, directory): def loader(path): if path is not None: - path = os.path.join(directory, path) + path = safe_join(directory, path) else: path = directory @@ -192,19 +193,11 @@ def generate_etag(self, mtime, file_size, real_filename): ) def __call__(self, environ, start_response): - cleaned_path = get_path_info(environ) + path = get_path_info(environ) if PY2: - cleaned_path = cleaned_path.encode(get_filesystem_encoding()) + path = path.encode(get_filesystem_encoding()) - # sanitize the path for non unix systems - cleaned_path = cleaned_path.strip("/") - - for sep in os.sep, os.altsep: - if sep and sep != "/": - cleaned_path = cleaned_path.replace(sep, "/") - - path = "/" + "/".join(x for x in cleaned_path.split("/") if x and x != "..") file_loader = None for search_path, loader in self.exports: diff --git a/src/werkzeug/security.py b/src/werkzeug/security.py index 1842afd0a..2308040d8 100644 --- a/src/werkzeug/security.py +++ b/src/werkzeug/security.py @@ -222,20 +222,28 @@ def check_password_hash(pwhash, password): def safe_join(directory, *pathnames): - """Safely join `directory` and one or more untrusted `pathnames`. If this - cannot be done, this function returns ``None``. + """Safely join zero or more untrusted path components to a base + directory to avoid escaping the base directory. - :param directory: the base directory. - :param pathnames: the untrusted pathnames relative to that directory. + :param directory: The trusted base directory. + :param pathnames: The untrusted path components relative to the + base directory. + :return: A safe path, otherwise ``None``. """ parts = [directory] + for filename in pathnames: if filename != "": filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - return None - if os.path.isabs(filename) or filename == ".." or filename.startswith("../"): + + if ( + any(sep in filename for sep in _os_alt_seps) + or os.path.isabs(filename) + or filename == ".." + or filename.startswith("../") + ): return None + parts.append(filename) + return posixpath.join(*parts)