Рубрики
Без рубрики

Общие подводные камни безопасности Python и как их избежать

Популярность Python привлекает как черных, так и белых хакеров. Вот как решения для статического анализа кода, такие как Bandit, могут помочь нам найти и исправить недостатки безопасности в наших приложениях на Python.

Автор оригинала: Saif Sadiq.

Python, несомненно, является популярным языком. Он неизменно входит в число самых популярных и любимых языков год после года . Это нетрудно объяснить, учитывая, насколько он беглый и выразительный. Его синтаксис, похожий на псевдокод, делает его чрезвычайно легким для начинающих, чтобы выбрать его в качестве своего первого языка, в то время как его обширная библиотека пакетов (включая такие гиганты, как Django и TensorFlow) гарантирует, что он масштабируется для любой требуемой от него задачи.

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

Проблемы и решения

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

Небезопасная десериализация

OWASP Top Ten , базовый контрольный список для веб-безопасности, упоминает небезопасную десериализацию как один из десяти наиболее распространенных недостатков безопасности. Хотя общеизвестно, что выполнение чего-либо, исходящего от пользователя, является ужасной идеей, сериализация и десериализация пользовательского ввода не кажутся одинаково серьезными. В конце концов, никакой код не запускается, верно? Неправильный,

PyYAML является де-факто стандартом для сериализации и десериализации YAML в Python. Библиотека поддерживает сериализацию пользовательских типов данных в YAML и десериализацию их обратно в объекты Python. Смотрите Этот код сериализации здесь и созданный им YAML.

# serialize.py
import yaml class Person: def __init__ (self, name, age): self.name = name self.age = age def __str__ (self): return f'' def __repr__ (self): return str(self) person = Person('Dhruv', 24)
with open('person.yml', 'w') as output_file: yaml.dump(person, output_file)
!!python/object: __main__.Person
age: 24
name: Dhruv

Десериализация этого YAML возвращает исходный тип данных.

# deserialize.py
import yaml # class Person needs to be present in the scope
with open('person.yml', 'r') as input_file: person = yaml.load(input_file, Loader = yaml.Loader) print(person)
$ python deserialize.py ↵

Как вы можете видеть, строка !!python/object: __main__.Person в YAML описывается, как повторно создавать объекты из их текстовых представлений. Но это открывает множество векторов атаки, которые могут перерасти в ГОНКУ , когда этот экземпляр может выполнять код.

Решение

Решение, каким бы тривиальным оно ни казалось, заключается в использовании безопасной загрузки путем замены загрузчика yaml.Погрузчик в пользу ямл.SafeLoader погрузчик. Этот загрузчик безопаснее, потому что он полностью блокирует загрузку пользовательских классов.

# deserialize.py
import yaml # class Person needs to be present in the scope
with open('person.yml', 'r') as input_file: person = yaml.load(input_file, Loader = yaml.SafeLoader) print(person)
$ python deserialize.py ↵
ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object: __main__.Person'
  in "person.yml", line 1, column 1

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

age: 24
name: Dhruv
$ python deserialize.py ↵
{'age': 24, 'name': 'Dhruv'}

Динамическое выполнение

Python имеет пару очень опасных функций: exec и eval . Оба они очень похожи с точки зрения того, что они делают: обрабатывают строки, переданные им в виде кода Python. exec ожидает, что строка будет оператором, который он выполнит и не вернет значение. eval ожидает, что строка будет выражением и вернет вычисленное значение выражения.

Вот пример того, что обе эти функции в действии.

eval('2 + 5') # returns 7
exec('print("Hello")') # prints "Hello", no return

Теоретически вы могли бы передать оператор в eval и получить аналогичный эффект , как exec , потому что в Python возврат None практически то же самое, что вообще ничего не возвращать.

eval('print("Hello")') # prints "Hello", returns None

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

Решение

Есть способы уменьшить доступ, который имеет eval. Вы можете ограничить доступ к глобальным и локальным объектам, передав словари в качестве второго и третьего аргументов в eval соответственно. Помните, что местные жители имеют приоритет над глобальными в случае конфликта.

x = 3
eval('x + 5') # returns 8
eval('x + 5', { 'x': 2 }) # returns 7
eval('x + 5', { 'x': 2 }, { 'x': 1 }) # returns 6

Это делает код более безопасным, да. По крайней мере, это несколько предотвращает утечку данных в переменных.

Но это все равно не мешает строке обращаться к любым встроенным модулям , таким как pow или, что более опасно, __import__ . Чтобы противостоять этому, вам нужно переопределить __builtins__ .

eval(" __import__ ('math').sqrt(5)", {}, {}) # returns 2.2360679774997898
eval( " __import__ ('math').sqrt(5)", { " __builtins__": None }, {} # restricts access to built-ins
) # error

Безопасно разоблачать сейчас? Не совсем. Потому что независимо от того , насколько безопасно вы делаете eval , его задача состоит в том, чтобы оценить выражение, и ничто не может помешать выражению занять слишком много времени и заморозить сервер на долгое время.

eval("2 ** 2147483647", { " __builtins__": None }, {}) # goodbye!

Как мы, разработчики Python, любим говорить: ” eval |/это зло .”

Управление зависимостями

Популярность Python привлекает внимание исследователей безопасности в белых шляпах так же, как и хакеров со злым умыслом. В результате появляются новые уязвимости в системе безопасности, которые постоянно обнаруживаются, раскрываются и исправляются. Чтобы держать злоумышленников в страхе, ваше программное обеспечение должно поддерживать все свои зависимости в актуальном состоянии.

Распространенным методом закрепления пакетов в Python является вездесущий requirements.txt файл, простой файл, в котором перечислены все зависимости и точные версии, необходимые для вашего проекта.

Допустим, вы устанавливаете Django. На момент написания статьи Django зависит еще от трех пакетов. Если вы заморозите свои зависимости, вы получите следующие требования. Обратите внимание, что только одна из этих зависимостей была установлена вами, остальные 3 являются зависимыми.

$ pip freeze ↵
asgiref==3.3.1
Django==3.1.5
pytz==2020.5
sqlparse==0.4.1

pip freeze не помещает зависимости в уровни, и это проблема. Для небольших проектов с несколькими зависимостями, которые вы можете отслеживать мысленно, это не имеет большого значения, но по мере роста ваших проектов будут расти и ваши зависимости верхнего уровня. Конфликты возникают, когда субзависимости перекрываются. Обновление отдельных зависимостей также является беспорядком, поскольку графическая связь этих зависимостей не ясна из обычного текстового файла.

Решение

Pipenv и Poetry – это два инструмента, которые помогут вам лучше управлять зависимостями. Я предпочитаю Пипенв, но поэзия не менее хороша. Оба менеджера пакетов строятся поверх pip . Интересный факт: Deep Source совместим как с Pipenv, так и с Poetry в качестве менеджеров пакетов.

Например, Pipenv отслеживает ваши зависимости верхнего уровня в файле Pipe , а затем выполняет тяжелую работу по блокировке зависимостей в имени файла блокировки Pipfile.блокировка , аналогично тому, как npm управляет Node.js пакеты. Вот пример Pip-файла .

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
django = "*"

[requires]
python_version = "3.9"

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

Вот тот же пример с Джанго. Обратите внимание, как Pipenv может идентифицировать ваши зависимости верхнего уровня, их зависимости и так далее.

$ pipenv graph ↵
Django==3.1.5
  - asgiref [required: >=3.2.10,<4, installed: 3.3.1]
  - pytz [required: Any, installed: 2020.5]
  - sqlparse [required: >=0.2.2, installed: 0.4.1]

Если ваш код размещен на GitHub, убедитесь, что вы также включили и настроили Надежность . Это изящный маленький бот, который предупреждает вас, если какая-либо из ваших зависимостей устарела или если в закрепленной версии зависимости была обнаружена уязвимость. Иждивенец также будет вносить PR в ваше репо, автоматически обновляя ваши пакеты. Действительно, очень удобно!

Утверждения во время выполнения

В Python есть специальное ключевое слово assert для защиты от неожиданных ситуаций. Цель assert проста: проверить условие и вызвать ошибку, если условие не выполнено. По сути, assert вычисляет данное выражение и

  • если он оценивает до истинного значения, движется вперед
  • если он оценивает значение falsey, вызывает AssertionError с данным сообщением

Рассмотрим этот пример.

def do_something_dangerous(user, command): assert user.has_permissions(command), f'{user} is not authorized' user.execute(command)

Это очень простой пример, в котором мы проверяем, есть ли у пользователя разрешения на выполнение действия, а затем выполняем данное действие. В этом случае, если user.has_permissions() возвращает False , ваше утверждение вызовет AssertionError и выполнение будет остановлено. Кажется довольно безопасным, не так ли?

Нет. Утверждения-это инструменты для разработчиков на этапах разработки и отладки. Утверждения не должны использоваться для защиты критических функций. Константа Python __debug__ имеет значение False во время компиляции, что удаляет операторы assert из скомпилированного кода для оптимизации кода для повышения производительности. Удаление операторов assert из скомпилированного кода оставляет функцию незащищенной.

Еще одна причина, по которой следует избегать assert s, заключается в том, что ошибки утверждений не полезны отладчикам, поскольку они не предоставляют никакой информации, кроме того факта, что утверждение не выдержало проверки. Определение классов исключений apt и последующее создание их экземпляров-гораздо более надежное решение.

Решение

Для альтернативного подхода вернитесь к основам. Вот та же самая программа, на этот раз использующая if/else и вызывающая Ошибку разрешения (вам нужно будет где-то ее определить), когда требуемое утверждение не выполняется.

def do_something_dangerous(user, command): if user.has_permissions(command): # [safer as it will not be removed by compiler user.execute(command) else: raise PermissionError(f'{user} is not authorized') # suitable error class

Этот код использует простые конструкции Python, работает так же с __debug__ set to True или False и вызывает четкие исключения, которые могут быть обработаны с гораздо большей ясностью.

Достижение Дзен

Главный вывод из всех этих примеров-никогда не доверять своим пользователям. Входные данные, предоставляемые пользователями, не должны быть сериализованы-десериализованы, оценены, выполнены или визуализированы. Чтобы быть в безопасности, вы должны быть осторожны с тем, что вы пишете, и тщательно проверять код после его написания.

Знаете ли вы, что лучше, чем сканирование на наличие уязвимостей в коде после его написания? Выделение уязвимостей сразу же после написания кода с их помощью.

Бандит

Инструменты статического анализа кода, такие как линтеры и сканеры уязвимостей, могут помочь вам найти множество проблем, прежде чем они будут использованы в дикой природе. Отличным инструментом для поиска уязвимостей безопасности в Python является Bandit . Бандит просматривает каждый файл, генерирует для него абстрактное синтаксическое дерево (AST), а затем запускает множество тестов для этого ПОСЛЕДНЕГО. Bandit может обнаружить целую кучу уязвимостей из коробки, а также может быть расширен для конкретных сценариев и совместимости с фреймворками с помощью плагинов. На самом деле, Bandit способен обнаружить все вышеупомянутые недостатки безопасности.

Если вы разработчик Python, я не могу рекомендовать Bandit достаточно. Я мог бы написать целую статью, превознося его достоинства. Я мог бы даже это сделать.

Глубокий Источник

Вы также должны рассмотреть возможность автоматизации всего этого процесса аудита и проверки с помощью инструментов автоматизации проверки кода, таких как DeepSource , которые сканируют ваш код при каждой фиксации и для каждого PR с помощью линтеров и анализаторов безопасности и могут автоматически устранять множество проблем. Deep Source также имеет свои собственные встроенные анализаторы для большинства языков, которые постоянно совершенствуются и обновляются. И это невероятно легко настроить !

version = 1

[[analyzers]]
name = "python"
enabled = true

  [analyzers.meta]
  runtime_version = "3.x.x"
  max_line_length = 80

Кто знал, что это будет так просто?

Испытайте дзен Питона и будьте осторожны, чтобы черные шляпы не нарушали ваш покой!