server: Check for relative paths to invalid directories
authorW. Trevor King <wking@tremily.us>
Thu, 20 Feb 2014 21:10:50 +0000 (13:10 -0800)
committerW. Trevor King <wking@tremily.us>
Thu, 20 Feb 2014 21:22:21 +0000 (13:22 -0800)
Avoid leaking information to requests like:

  http://localhost:4000/../../etc/passwd

PEP 333 isn't clear on what values are allowed for PATH_INFO, but it
does mention them as "CGI-style" [1].  RFC 3875, defining CGI 1.1,
says about PATH_INFO [2]:

  The server MAY impose restrictions and limitations on what values it
  permits for PATH_INFO, and MAY reject the request with an error if
  it encounters any values considered objectionable.

I can't actually exploit this with Python's reference WSGI
implementation.  When I tried to fetch /../../etc/passwd with Wget, I
got '/etc/passwd' as PATH_INFO, but this seems like an
important-enough risk that a little extra checking would not be wrong
;).

Also drop the urlparse call, because PATH_INFO is already the parsed
path portion of the URL.

[1]: http://legacy.python.org/dev/peps/pep-0333/#specification-details
[2]: http://tools.ietf.org/search/rfc3875#section-4.1.5

package_cache/server.py

index be34ee4960e9925eac38432144bb6244c60ebb77..1da14d645b55335e61e141b010dba0f88b8f98e8 100644 (file)
@@ -4,7 +4,6 @@ import email.utils as _email_utils
 import mimetypes as _mimetypes
 import os as _os
 import urllib.error as _urllib_error
-import urllib.parse as _urllib_parse
 import urllib.request as _urllib_request
 
 from . import __version__
@@ -41,9 +40,7 @@ class Server (object):
         url = environ.get('PATH_INFO', None)
         if url is None:
             raise InvalidFile(url=url)
-        parsed_url = _urllib_parse.urlparse(url)
-        relative_path = parsed_url.path.lstrip('/').replace('/', _os.path.sep)
-        cache_path = _os.path.join(self.cache, relative_path)
+        cache_path = self._get_cache_path(url=url)
         if not _os.path.exists(path=cache_path):
             self._get_file_from_sources(url=url, path=cache_path)
         if not _os.path.isfile(path=cache_path):
@@ -51,6 +48,15 @@ class Server (object):
         return self._serve_file(
             path=cache_path, environ=environ, start_response=start_response)
 
+    def _get_cache_path(self, url):
+        relative_path = url.lstrip('/').replace('/', _os.path.sep)
+        cache_path = _os.path.abspath(_os.path.join(self.cache, relative_path))
+        check_relative_path = _os.path.relpath(
+            path=cache_path, start=self.cache)
+        if check_relative_path.startswith(_os.pardir + _os.path.sep):
+            raise InvalidFile(url=url)
+        return cache_path
+
     def _get_file_from_sources(self, url, path):
         dirname = _os.path.dirname(path)
         if not _os.path.isdir(dirname):