Предотвращает ли мой код обход каталога?
Является ли следующий фрагмент кода из приложения 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'