Skip to content

Commit d9fdf55

Browse files
committed
Merge pull request #383 from magv/master
Validate full file path in the static file handler
2 parents 7331590 + d108b7f commit d9fdf55

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

aiohttp/web_urldispatcher.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,13 @@ def __init__(self, name, prefix, directory, *,
149149
'GET', self.handle, name, expect_handler=expect_handler)
150150
self._prefix = prefix
151151
self._prefix_len = len(self._prefix)
152-
self._directory = directory
152+
self._directory = os.path.abspath(directory) + os.sep
153153
self._chunk_size = chunk_size
154154

155+
if not os.path.isdir(self._directory):
156+
raise ValueError(
157+
"No directory exists at '{}'".format(self._directory))
158+
155159
def match(self, path):
156160
if not path.startswith(self._prefix):
157161
return None
@@ -167,13 +171,13 @@ def url(self, *, filename, query=None):
167171
def handle(self, request):
168172
resp = StreamResponse()
169173
filename = request.match_info['filename']
170-
filepath = os.path.join(self._directory, filename)
171-
if '..' in filename:
174+
filepath = os.path.abspath(os.path.join(self._directory, filename))
175+
if not filepath.startswith(self._directory):
172176
raise HTTPNotFound()
173177
if not os.path.exists(filepath) or not os.path.isfile(filepath):
174178
raise HTTPNotFound()
175179

176-
ct, encoding = mimetypes.guess_type(filename)
180+
ct, encoding = mimetypes.guess_type(filepath)
177181
if not ct:
178182
ct = 'application/octet-stream'
179183
resp.content_type = ct
@@ -394,8 +398,6 @@ def add_static(self, prefix, path, *, name=None, expect_handler=None,
394398
:param path - folder with files
395399
"""
396400
assert prefix.startswith('/')
397-
assert os.path.isdir(path), 'Path does not directory %s' % path
398-
path = os.path.abspath(path)
399401
if not prefix.endswith('/'):
400402
prefix += '/'
401403
route = StaticRoute(name, prefix, path,

tests/test_web_functional.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def go(dirname, filename):
287287
resp.close()
288288

289289
here = os.path.dirname(__file__)
290-
filename = os.path.join(here, 'data.unknown_mime_type')
290+
filename = 'data.unknown_mime_type'
291291
self.loop.run_until_complete(go(here, filename))
292292

293293
def test_static_file_with_content_type(self):
@@ -319,7 +319,7 @@ def go(dirname, filename):
319319
resp.close()
320320

321321
here = os.path.dirname(__file__)
322-
filename = os.path.join(here, 'software_development_in_picture.jpg')
322+
filename = 'software_development_in_picture.jpg'
323323
self.loop.run_until_complete(go(here, filename))
324324

325325
def test_static_file_with_content_encoding(self):
@@ -342,9 +342,46 @@ def go(dirname, filename):
342342
resp.close()
343343

344344
here = os.path.dirname(__file__)
345-
filename = os.path.join(here, 'hello.txt.gz')
345+
filename = 'hello.txt.gz'
346346
self.loop.run_until_complete(go(here, filename))
347347

348+
def test_static_file_directory_traversal_attack(self):
349+
350+
@asyncio.coroutine
351+
def go(dirname, relpath):
352+
self.assertTrue(os.path.isfile(os.path.join(dirname, relpath)))
353+
354+
app, _, url = yield from self.create_server('GET', '/static/')
355+
app.router.add_static('/static', dirname)
356+
357+
url_relpath = url + relpath
358+
resp = yield from request('GET', url_relpath, loop=self.loop)
359+
self.assertEqual(404, resp.status)
360+
resp.close()
361+
362+
url_relpath2 = url + 'dir/../' + filename
363+
resp = yield from request('GET', url_relpath2, loop=self.loop)
364+
self.assertEqual(404, resp.status)
365+
resp.close()
366+
367+
url_abspath = \
368+
url + os.path.abspath(os.path.join(dirname, filename))
369+
resp = yield from request('GET', url_abspath, loop=self.loop)
370+
self.assertEqual(404, resp.status)
371+
resp.close()
372+
373+
here = os.path.dirname(__file__)
374+
filename = '../README.rst'
375+
self.loop.run_until_complete(go(here, filename))
376+
377+
def test_static_route_path_existence_check(self):
378+
directory = os.path.dirname(__file__)
379+
web.StaticRoute(None, "/", directory)
380+
381+
nodirectory = os.path.join(directory, "nonexistent-uPNiOEAg5d")
382+
with self.assertRaises(ValueError):
383+
web.StaticRoute(None, "/", nodirectory)
384+
348385
def test_post_form_with_duplicate_keys(self):
349386

350387
@asyncio.coroutine

0 commit comments

Comments
 (0)