[Перевод] Как защитить Python-приложения от внедрения вредоносных скриптов
Python-приложения используют множество скриптов. Этим и пользуются злоумышленники, чтобы подложить нам «свинью» — туда, где мы меньше всего ожидаем её увидеть.
Одним из достоинств Python считается простота использования: чтобы запустить скрипт, нужно просто сохранить его в .py
-файле и выполнить команду python с этим файлом (например, python my_file.py)
. Так же легко разбить наш файл, например, на модули my_app.py
и my_lib.py
и далее для подключения модулей использовать конструкцию import...from: import my_lib from my_app.py
.
Однако у этой простоты и лёгкости есть и обратная сторона: чем проще вам выполнять код из разных локаций, тем больше у злоумышленника возможностей для вмешательства.
Python-код нужно размещать в безопасных местах
Модель безопасности Python основана на трёх принципах:
- Предполагается, что каждая запись в sys.path должна указывать на надёжную локацию, откуда можно безопасно выполнять произвольный код.
- Каталог, в котором находится главный скрипт (main), всегда указан в sys.path.
- При вызове команды
python
текущий каталог рассматривается как локация главного скрипта, даже с параметрами-c
или-m
.
Предположим, вы хотите запустить Python-приложение, которое было «правильно» установлено на вашу машину. В этом случае единственная локация (кроме папки с установленным Python), которая будет автоматически добавлена в ваш sys.path по умолчанию, — это папка с главным скриптом или исполняемым файлом.
Например, если pip
находится в /usr/bin
, и вы запускаете /usr/bin/pip
, то в sys.path
будет добавлен только /usr/bin
. Записывать файлы в /usr/bin может только root, поэтому эта локация считается безопасной.
Тем не менее, соглашения предписывают нам поступать таким образом: /path/to/python -m pip
. Это позволяет избежать проблем с $PATH
и противоречий с документацией для Windows.
В общем, всё и так будет хорошо, если только у вас одного есть полномочия добавлять и редактировать файлы в директориях, из которых Python берёт модули для импорта.
Папка Downloads — уязвимая локация
Существует множество способов заставить браузеры (а иногда и другое программное обеспечение) поместить файлы с произвольными именами в папку Downloads («Загрузки») без ведома пользователя. Эту уязвимость использует атака под названием «Подмена DLL». В нашем случае речь пойдёт о скриптах Python, но идея та же самая.
Браузеры стали более серьёзно относиться к этому и постепенно пытаются сделать так, чтобы посещённые пользователем сайты не могли тайно сбрасывать файлы в его папку Downloads.
Однако эту проблему будет очень трудно решить полностью: например, всё ещё доступен параметр Content-Disposition HTTP header’s filename*
, который позволяет сайтам выбрать каталог для размещения загруженных файлов.
Как работает атака
Вы запускаете установку: python -m pip
. Вы загружаете пакет Python с вполне заслуживающего доверия веб-сайта, который, правда, по какой-то причине предлагает загрузку напрямую, а не через PyPI. Может быть, это какая-то внутренняя версия, может быть, это предварительный релиз — без разницы. Итак к вам отправляется totally-legit-package.whl
:
~$ cd Downloads
~/Downloads$ python -m pip install ./totally-legit-package.whl
Вроде бы ничего страшного, но, оказывается, две недели назад на совершенно другом сайте, который вы посетили, какой-то XSS JavaScript без вашего ведома загрузил pip.py с вредоносными программами в вашу папку Downloads.
Бум!
Авария!
~$ mkdir attacker_dir
~$ cd attacker_dir
~/attacker_dir$ echo 'print("lol ur pwnt")' > pip.py
~/attacker_dir$ python -m pip install requests
lol ur pwnt
Странное поведение PYTHONPATH
Несколькими абзацами выше я писал:
единственная локация (кроме папки с установленным Python), которая будет автоматически добавлена в ваш sys.path по умолчанию, — это папка с главным скриптом или исполняемым файлом.
Так, а что здесь делает словосочетание «по умолчанию»? Какие ещё локации можно добавить?
В принципе в переменную окружения $PYTHONPATH
можно записать что угодно. Но вы бы не стали записывать свою текущую локацию в $PYTHONPATH
, правда?
К сожалению, существует ситуация, при которой вы могли бы сделать это случайно.
Давайте для иллюстрации набросаем «уязвимое» Python-приложение:
# tool.py
try:
import optional_extra
except ImportError:
print("extra not found, that's fine")
Создадим 2 директории: install_dir
и attacker_dir
. Провалимся в install_dir
, затем выполним cd attacker_dir
и разместим там наш вредоносный код по названием tool.py
:
# optional_extra.py
print("lol ur pwnt")
Останется только запустить его:
~/attacker_dir$ python ../install_dir/tool.py
extra not found, that's fine
Пока всё идёт неплохо.
Но сейчас мы увидим распространённую ошибку. Многие рекомендуют добавить в $PYTHONPATH
ещё кое-что:
export PYTHONPATH="/new/useful/stuff:$PYTHONPATH";
На первый взгляд, это имеет смысл: если ты добавляешь проект X в $PYTHONPATH
, возможно, по проекту Y туда тоже что-то добавили, а может, и нет; в любом случае вам бы не хотелось затереть изменения, связанные с другими проектами. Особенно это важно, когда вы пишите документацию, которую будут использовать много людей.
И вот теперь мы сталкиваемся со «странным» поведением $PYTHONPATH
. Если до первого запуска $PYTHONPATH
была пуста или не установлена, в ней появится пустая строка, которая будет распознана как текущий каталог. Проверим это:
~/attacker_dir$ export PYTHONPATH="/a/perfectly/safe/place:$PYTHONPATH";
~/attacker_dir$ python ../install_dir/tool.py
lol ur pwnt
Для пущей безопасности давайте сделаем $PYTHONPATH
пустой и попробуем ещё раз:
~/attacker_dir$ export PYTHONPATH="";
~/attacker_dir$ python ../install_dir/tool.py
lol ur pwnt
Точно, это совсем не безопасно!
И ещё: оказывается, ситуации, когда переменная $PYTHONPATH
пуста и когда переменная $PYTHONPATH
не установлена, — это две разные истории:
os.environ.get("PYTHONPATH") == ""
и os.environ.get("PYTHONPATH") == None.
Если вы хотите быть уверенными, что очистили $PYTHONPATH
, используйте команду unset
:
~/attacker_dir$ python ../install_dir/tool.py
extra not found, that's fine
Вообще, запись в переменную $PYTHONPATH
— был самым распространённым способом настройки окружения для работы Python-приложений. К счастью, он вышел из моды с появлением виртуальных окружений и virtualenv. Если у вас старая конфигурация, которая что-то пишет в $PYTHONPATH
, но вы можете обойтись без этого, то сейчас самое время удалить её.
Если по каким-то причинам вы этого сделать не можете, то используйте лайфхак:
export PYTHONPATH="${PYTHONPATH:+${PYTHONPATH}:}new_entry_1"
export PYTHONPATH="${PYTHONPATH:+${PYTHONPATH}:}new_entry_2"
В bash и zsh это даст вот такой результат:
$ echo "${PYTHONPATH}"
new_entry_1:new_entry_2
Ну вот, теперь нет никаких лишних двоеточий и пустых записей.
И в конце: если вы всё ещё работаете с $PYTHONPATH
, используйте абсолютные пути. Всегда!
Проблема гораздо серьёзнее
Существует несколько вариантов опасного развития событий, связанных с запуском Python-файлов из папки Downloads:
- Запуск python
~/Downloads/anything.py
(даже если самanything.py
безопасен). Ваша папка Downloads будет добавлена вsys.path
. - После запуска
jupyter notebook ~/Downloads/anything.ipynb
папка Downloads также будет добавлена вsys.path
.
Поэтому перед запуском файлы .py и .ipynb лучше убрать из этой папки.
Выполнение cd Downloads
и последующий запуск python -c
с инструкцией import
внутри или интерактивный запуск python
с последующим импортом не решают проблему до конца, если импортируемые файлы лежат в папке Downloads.
К сожалению, ~/Downloads/
не единственная локация, которая может содержать вредоносные файлы. Например, если вы администрируете сервер на который пользователи могут загружать файлы, проверьте, чтобы никто и ничто не могло запустить cd public_uploads
перед запуском команды python
.
В этом случае, возможно, стоит предусмотреть, чтобы код, который обрабатывает загрузку файлов, добавлял к их именам .uploaded
. Это поможет избежать незапланированного выполнения скриптов .py
.
Предостережения
Если у вас есть приложения, написанные на Python, которые вы хотите использовать, находясь в папке Downloads, возьмите за правило вводить путь к скрипту (/ path / to / venv / bin / pip
), а не к модулю (/ path / to / venv / bin / python -m pip
).
В общем, просто избегайте использования Downloads в качестве текущего рабочего каталога и любые скрипты, которые вы хотите использовать, перед запуском переместите в более подходящее место.
Важно понимать, откуда Python берёт код, который будет выполнять. Позволить кому-либо выполнить хотя бы одну строку левого Python-скрипта равносильно предоставлению полного контроля над вашим компьютером!
Производственная необходимость
Читая такую статью с «советами и лайфхаками» по безопасности очень легко предположить, что автор очень умён, знает кучу мелочей, которую и другие должны знать и постоянно думать об этом. Но на самом деле, это не совсем так. Я объясню.
За многие годы работы я с завидной периодичностью наблюдал, как пользователи не понимали, откуда Python загружает код. Например, люди помещают свою первую программу, использующую Twisted, в файл с именем twisted.py. Но ведь в этом случае импорт библиотеки просто невозможен!
Киберпреступники радуются намного больше, если они смогли успешно атаковать системных администраторов или разработчиков. Если вы взломаете пользователя, то получите данные этого пользователя. Но если вы взломаете администратора или разработчика и сделаете это правильно, то вы можете получить доступ к тысячам пользователей, чьи системы находятся под контролем администратора, или даже миллионам пользователей, которые используют программное обеспечение разработчиков.
Поэтому «просто всё время будь осторожнее» не является надёжным рецептом. Те ИТ-специалисты, которые несут ответственность за продукты с большим количеством пользователей, действительно обязаны быть более осторожными. По крайней мере, должны быть проинформированы об особенностях работы тех или иных инструментов разработки.
Баг или фича…
Ничего из того, что я описал выше, на самом деле не является настоящей «ошибкой» или «уязвимостью». Я не думаю, что разработчики Python или Jupyter сделали что-то по ошибке. Система работает так, как она спроектирована, и это, наверное, можно объяснить. Лично у меня нет идей, как можно что-то в ней изменить, не урезая ту мощь Python, за которую мы так его ценим.
Одно из моих любимых изобретений — это SawStop, уникальная система безопасности для настольных пил. Она позволяет останавливать вращение пильного диска в течении 5 миллисекунд — при контакте с человеческим телом. До изобретения этой системы настольные пилы были инструментами чрезвычайно опасными, но они выполняли важную производственную функцию. На настольных пилах было сделано много очень полезных и важных вещей. Тем не менее, верно также и то, что на настольные пилы приходилась непропорционально большая доля несчастных случаев в деревообрабатывающих цехах. Теперь SawStop экономит много пальцев каждый год.
Таким образом, подробно описав сложности при работе со скриптами Python, я также надеюсь натолкнуть на размышления некоторых инженеров по безопасности. Можно ли сделать SawStop для выполнения произвольного кода для интерактивных интерпретаторов? Какое решение могло бы предотвратить некоторые из описанных выше проблем?
Оставайтесь в безопасности, друзья.
На правах рекламы
VDSina предлагает безопасные серверы на Linux или Windows — выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.