diff -r 7323a865457a Doc/library/http.server.rst --- a/Doc/library/http.server.rst Sun Jun 05 20:43:30 2011 +0200 +++ b/Doc/library/http.server.rst Sun Jun 12 20:45:11 2011 -0400 @@ -308,7 +308,10 @@ .. method:: do_GET() The request is mapped to a local file by interpreting the request as a - path relative to the current working directory. + path relative to the current working directory. Relative paths are + translated. If the translated path points higher in the directory + hierarchy than the current working directory, a ``400`` error response is + returned. If the request was mapped to a directory, the directory is checked for a file named ``index.html`` or ``index.htm`` (in that order). If found, the @@ -334,6 +337,8 @@ For example usage, see the implementation of the :func:`test` function invocation in the :mod:`http.server` module. + .. versionchanged:: 3.3 + Relative paths are now translated and checked. The :class:`SimpleHTTPRequestHandler` class can be used in the following manner in order to create a very basic webserver serving files relative to diff -r 7323a865457a Lib/http/server.py --- a/Lib/http/server.py Sun Jun 05 20:43:30 2011 +0200 +++ b/Lib/http/server.py Sun Jun 12 20:45:11 2011 -0400 @@ -688,7 +688,13 @@ None, in which case the caller has nothing further to do. """ - path = self.translate_path(self.path) + + try: + path = self.translate_path(self.path) + except IndexError: + self.send_error(400) + return + f = None if os.path.isdir(path): if not self.path.endswith('/'): @@ -763,26 +769,21 @@ return f def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. + """Translate *path* to the local filename syntax. - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - + Relative path traversal (using '..' and '.') is permitted. Raises an + IndexError if the translated path would fall above the server's root + directory. """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - path = posixpath.normpath(urllib.parse.unquote(path)) - words = path.split('/') - words = filter(None, words) - path = os.getcwd() - for word in words: - drive, word = os.path.splitdrive(word) - head, word = os.path.split(word) - if word in (os.curdir, os.pardir): continue - path = os.path.join(path, word) - return path + translated_path = '/'.join(_url_collapse_path_split(path)).lstrip('/') + translated_path = urllib.parse.unquote(translated_path) + # Normalize the path and compute its relative path from the current + # directory. If it contains os.pardir, it points above the current + # directory and cannot be securely translated. + relative_to_curdir = os.path.relpath(os.path.normpath(translated_path)) + if os.pardir in relative_to_curdir.split(os.sep): + raise IndexError("Path %s points above server root." % path) + return os.path.join(os.getcwd(), translated_path) def copyfile(self, source, outputfile): """Copy all data between two file objects. @@ -851,30 +852,28 @@ Raises: IndexError if too many '..' occur within the path. """ - # Similar to os.path.split(os.path.normpath(path)) but specific to URL - # path semantics rather than local operating system semantics. - path_parts = [] - for part in path.split('/'): - if part == '.': - path_parts.append('') + # trim the query and fragment components, if present + for path_delimeter in ('?', '#'): + path = path.split(path_delimeter, 1)[0] + if '/' in path: + head, tail = path.rsplit('/', 1) + else: + head, tail = '', path + + fragments = (fragment for fragment in head.split('/') if fragment) + translated_path = [] + for fragment in fragments: + if fragment == '..': + translated_path.pop() + elif fragment == '.': + continue else: - path_parts.append(part) - # Filter out blank non trailing parts before consuming the '..'. - path_parts = [part for part in path_parts[:-1] if part] + path_parts[-1:] - if path_parts: - tail_part = path_parts.pop() - else: - tail_part = '' - head_parts = [] - for part in path_parts: - if part == '..': - head_parts.pop() - else: - head_parts.append(part) - if tail_part and tail_part == '..': - head_parts.pop() - tail_part = '' - return ('/' + '/'.join(head_parts), tail_part) + translated_path.append(fragment) + if tail == '..': + translated_path.pop() + tail = '' + head = '/' + '/'.join(translated_path) + return (head, tail) nobody = None @@ -975,7 +974,11 @@ def run_cgi(self): """Execute a CGI script.""" - path = self.path + try: + path = self.translate_path(self.path) + except IndexError: + self.send_error(400) + return dir, rest = self.cgi_info i = path.find('/', len(dir) + 1) diff -r 7323a865457a Lib/test/test_httpservers.py --- a/Lib/test/test_httpservers.py Sun Jun 05 20:43:30 2011 +0200 +++ b/Lib/test/test_httpservers.py Sun Jun 12 20:45:11 2011 -0400 @@ -256,6 +256,10 @@ self.check_status_and_reason(response, 404) response = self.request('/' + 'ThisDoesNotExist' + '/') self.check_status_and_reason(response, 404) + response = self.request('/../test') + self.check_status_and_reason(response, 400) + response = self.request('/test/../../') + self.check_status_and_reason(response, 400) with open(os.path.join(self.tempdir_name, 'index.html'), 'w') as f: response = self.request('/' + self.tempdir_name + '/') self.check_status_and_reason(response, 200) @@ -638,8 +642,9 @@ class SimpleHTTPRequestHandlerTestCase(unittest.TestCase): """ Test url parsing """ def setUp(self): - self.translated = os.getcwd() - self.translated = os.path.join(self.translated, 'filename') + self.cwd = os.getcwd() + self.translated = os.path.join(self.cwd, 'filename') + self.nested = os.path.join(self.cwd, os.sep.join(('foo', 'bar'))) self.handler = SocketlessRequestHandler() def test_query_arguments(self): @@ -656,6 +661,28 @@ path = self.handler.translate_path('//filename?foo=bar') self.assertEqual(path, self.translated) + def test_relative_paths(self): + path = self.handler.translate_path('/foo/./bar') + self.assertEqual(path, self.nested) + path = self.handler.translate_path('/foo/../foo/bar') + self.assertEqual(path, self.nested) + path = self.handler.translate_path('/./foo/bar') + self.assertEqual(path, self.nested) + path = self.handler.translate_path(os.sep.join(( + 'foo', os.pardir, 'foo', 'bar'))) + self.assertEqual(path, self.nested) + path = self.handler.translate_path(os.sep.join(( + 'foo', os.curdir, 'bar'))) + self.assertEqual(path, self.nested) + + def test_chroot(self): + self.assertRaises(IndexError, self.handler.translate_path, + '/../foo/bar') + self.assertRaises(IndexError, self.handler.translate_path, + os.sep.join((os.pardir, 'foo'))) + self.assertRaises(IndexError, self.handler.translate_path, + os.sep.join(('foo', os.pardir, os.pardir))) + def test_main(verbose=None): cwd = os.getcwd() diff -r 7323a865457a Misc/ACKS --- a/Misc/ACKS Sun Jun 05 20:43:30 2011 +0200 +++ b/Misc/ACKS Sun Jun 12 20:45:11 2011 -0400 @@ -566,6 +566,7 @@ Per Lindqvist Eric Lindvall Gregor Lingl +Ori Livneh Nick Lockwood Stephanie Lockwood Anne Lord