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

Безопасен ли следующий фрагмент кода из приложения 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

person deamon    schedule 23.07.2011    source источник
comment
Что произойдет, если вы дадите ему абсолютный путь?   -  person Katriel    schedule 24.07.2011
comment
Хорошая идея! Ваше предложение привело меня к ответу: это небезопасно! Обход каталога был случайно предотвращен другой частью используемого фреймворка.   -  person deamon    schedule 24.07.2011


Ответы (3)


Ваш код не предотвращает обход каталога. Вы можете защититься от этого с помощью модуля 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
person jterrace    schedule 23.07.2011
comment
Просто не полагайтесь на действия, связанные с текущим рабочим каталогом, поскольку в веб-приложении это может быть что угодно. Всегда следует основывать его на абсолютном пути в качестве отправной точки, будь то жестко связанный или рассчитанный из __file__. - person Graham Dumpleton; 24.07.2011
comment
@ graham-dumpleton Это не имеет ничего общего с относительным или абсолютным. Путь всегда может выйти из строя, если вы не сделаете этот тип проверки работоспособности. - person jterrace; 24.07.2011
comment
Вы, кажется, не совсем понимаете, о чем я говорю. В своем примере вы указали «startdir = os.path.abspath (os.curdir)». Это установит startdir в текущий рабочий каталог. В веб-приложении Python нет никаких гарантий, каким будет текущий рабочий каталог. Таким образом, это плохой пример для использования, потому что люди будут слепо копировать и вставлять код, не понимая, что они должны привязывать его к абсолютному пути, который имеет значение для их приложения, а не полагаться на то, что os.getcwd() собирается вернуть когда вызывается os.path.abspath(os.curdir). - person Graham Dumpleton; 24.07.2011
comment
Вы можете протестировать этот фрагмент кода на фаззере атаки обхода каталога, таком как dotdotpwn @ github.com/wireghoul/dotdotpwn, напр. './dotdotpwn.pl -m http-url -u google.com/?q=TRAVERSAL -О -к корень:' - person evandrix; 27.03.2015
comment
Обратите внимание, что это зависит от того, что startdir заканчивается косой чертой, чего в данном случае нет! Этот код неправильно примет ввод ../jterrace-attack/foobar. - person Flimm; 02.02.2016

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

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'
person Clodoaldo Neto    schedule 23.07.2011
comment
Это не препятствует обходу каталога, поскольку file_name может содержать ../! Но ваш код все равно был полезен. - person deamon; 24.07.2011
comment
Извините, похоже, ваше решение тоже работает. Но это будет ограничено одним уровнем каталога (без подкаталогов). Я исправлю свой отрицательный голос, если вы немного измените свой ответ, чтобы я мог снова проголосовать). - person deamon; 24.07.2011
comment
@daemon os.path.basename убирает ../ с пути. Проверьте ответ обновления. - person Clodoaldo Neto; 28.03.2015
comment
os.path.basename удаляет ../, но также удаляет всю информацию, связанную с путем. os.path.basename("a/b/c") возвращает "c" - person Flimm; 02.02.2016
comment
@Flimm: Да, так и задумано. - person Clodoaldo Neto; 07.02.2016
comment
Нравится этот метод, потому что он прост и работает правильно, когда вы хотите работать в фиксированном каталоге (без подкаталогов) и получать от пользователя только имя файла. - person Augusto Destrero; 28.12.2016

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

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

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

person Tom Viner    schedule 08.11.2016