Предотвращает ли мой код обход каталога?

Является ли следующий фрагмент кода из приложения Python WSGI безопасным от обхода каталога? Он читает имя файла, переданное как параметр, и возвращает именованный файл.

file_name = request.path_params["file"]
file = open(file_name, "rb")
mime_type = mimetypes.guess_type(file_name)[0]
start_response(status.OK, [('Content-Type', mime_type)])
return file

Я установил приложение под http://localhost:8000/file/{file} и отправил запросы с URL http://localhost:8000/file/../alarm.gif и http://localhost:8000/file/%2e%2e%2falarm.gif. Но ни один из моих попыток не доставил (существующий) файл. Так мой код уже безопасен от обхода каталога?

Новый подход

Кажется, что следующий код предотвращает обход каталога:

file_name = request.path_params["file"]
absolute_path = os.path.join(self.base_directory, file_name)
normalized_path = os.path.normpath(absolute_path)

# security check to prevent directory traversal
if not normalized_path.startswith(self.base_directory):
    raise IOError()

file = open(normalized_path, "rb")
mime_type = mimetypes.guess_type(normalized_path)[0]
start_response(status.OK, [('Content-Type', mime_type)])
return file

Ответы

Ответ 1

Ваш код не предотвращает обход каталога. Вы можете предотвратить это с помощью os.path.

>>> import os.path
>>> os.curdir
'.'
>>> startdir = os.path.abspath(os.curdir)
>>> startdir
'/home/jterrace'

startdir теперь является абсолютным путем, когда вы не хотите, чтобы путь вышел за пределы. Теперь предположим, что мы получаем имя файла от пользователя, и они дают нам злонамеренный /etc/passwd.

>>> filename = "/etc/passwd"
>>> requested_path = os.path.relpath(filename, startdir)
>>> requested_path
'../../etc/passwd'
>>> requested_path = os.path.abspath(requested_path)
>>> requested_path
'/etc/passwd'

Теперь мы превратили свой путь в абсолютный путь относительно нашего начального пути. Поскольку это не было в исходном пути, у него нет префикса нашего начального пути.

>>> os.path.commonprefix([requested_path, startdir])
'/'

Вы можете проверить это в своем коде. Если функция commonprefix возвращает путь, который не начинается с startdir, тогда путь недействителен, и вы не должны возвращать содержимое.


Вышеуказанное может быть перенесено на статический метод:

import os 

def is_directory_traversal(file_name):
    current_directory = os.path.abspath(os.curdir)
    requested_path = os.path.relpath(file_name, start=current_directory)
    requested_path = os.path.abspath(requested_path)
    common_prefix = os.path.commonprefix([requested_path, current_directory])
    return common_prefix != current_directory

Ответ 2

Здесь гораздо более простое решение:

relative_path = os.path.relpath(path, start=self.test_directory)
has_dir_traversal = relative_path.startswith(os.pardir)

relpath заботится о нормализации пути для нас. И если относительный путь начинается с .., вы не разрешаете его.

Ответ 3

Используйте только базовое имя введенного пользователем файла:

file_name = request.path_params["file"]
file_name = os.path.basename(file_name)
file = open(os.path.join("/path", file_name), "rb")

os.path.basename разделите ../ на путь:

>>> os.path.basename('../../filename')
'filename'