Язык программирования Python получил широкую популярность среди разработчиков благодаря богатому функционалу и гибкости. Однако, как и у любого другого языка в Python имеются свои недостатки, связанные в том числе с безопасной разработкой.
Python является интерпретируемым языком, он не работает напрямую с памятью, как тот же Ассемблер или С, поэтому такие старые атаки, как переполнение буфера здесь работать не будут. Но в этом языке также имеется целый ряд небезопасных функций, которые лучше вообще не использовать, или использовать с определенными ограничениями.
Инъекции, и не только SQL
Большинство читателей наверняка слышали об SQL инъекциях, так как это наиболее распространенный вид инъекций. Но мы сейчас будем говорить об инъекции команд. Эти атаки менее популярны, потому что для их организации требуется больше времени и внимания. Однако игнорирование атак с использованием команд может сделать вашу систему или приложение уязвимыми для некоторых серьезных угроз. А в некоторых случаях это может даже привести к полной компрометации системы.
Начнем с использования функции eval. Допустим, у нас есть простой скрипт калькулятора на Python, который принимает выражение от пользователя и вычисляет выходные данные.
compute = input('\nYour expression? => ')
if not compute:
print ("No input")
else:
print ("Result =", eval(compute))
Логично, что ввод выражения типа 2 * 3 приведет к результату = 6, что является желаемым результатом. Но никто не обещает, что пользователи будут вводить только корректные математические выражения. Злоумышленник, с другой стороны, может ввести что–то вроде __import__(‘os’).system(‘rm -rf /’)
в качестве входных данных. И это приведет к удалению всех файлов и каталогов в папке скрипта, если у процесса достаточно прав.
Давайте также рассмотрим пример с функцией exec(). У нас есть следующий скрипт, который запускает игровую площадку, позволяющую новичкам играть с помощью простых команд Python, которые они выучили.
code = input('What command(s) in python did you learn today?')
exec(code)
Здесь также сознательный пользователь может ввести к примеру len(word) и получить 4, как и предполагалось. Но с другой стороны, не очень дружелюбный пользователь также может ввести __import__('os').system.listdir()
и просмотреть список всех ваших каталогов. Очевидно, что при наличии прав эту атаку можно развить.
Приведенные примеры конечно являются максимально упрощенными, но в реальном коде тоже можно встретить подобные ошибки. Для того, чтобы их избежать нам необходимо проверять пользовательские данные.
В примере с eval() нам поможет функция validate. Данная функция просто проверит все предоставленные данные на соответствие белому списку, содержащему только числовые символы и арифметические операторы.
compute = input('\nYour expression? => ')
if not compute :
print ("No input")
else:
if validate(compute):
print ("Result =", eval(compute))
else:
print ("Error")
Для примера с функцией exec() нам также поможет validate().
Команды ОС
Начинающий разработчик может полениться и отдать на откуп пользователю ввод необходимых для работы приложения параметров. Рассмотрим классический пример с использованием команды ping. Допустим наше приложение спрашивает у пользователя адрес узла, который нужно пропинговать.
address = request.args.get("address")
cmd = "ping -c 1 %s" % address
subprocess.Popen(cmd, shell=True)
Здесь уязвимость достаточно очевидна, и любая команда, которую мы вводим в качестве адреса, выполняется на сервере приложений. Все, что нужно сделать злоумышленнику, это добавить разделитель, а затем ввести любые команды, которые он хочет. Например, google.com ; ls -la
.
Здесь проблемным элементом является использование subprocess.Popen() и значение ключа shell=True. По сути наша программа готова выполнить любую команду введенную пользователем.
Однако исправить это несложно, поскольку Python предоставляет встроенные функции для устранения подобных проблем.
address = request.args.get("address")
command = "ping -c 1 {}".format(address)
args = shlex.split(command)
subprocess.Popen(args)
Функция shlex.split() преобразует командную строку в массив перед ее запуском. Таким образом, при наличии каких-либо вредоносных данных выполнение команды завершится ошибкой.
Посмотрим еще несколько примеров инъекций в Python. Так команда popen (а также pop2, popen3, popen4) выполняют переданную строку как команду, что создает возможность для инъекции:
import os
user_input = "foo && cat /etc/passwd"
os.popen("ls -l " + user_input)
В этом примере user_input это то, что ввел пользователь. Далее в строке с os.popen мы пытаемся выполнить ls –l с пользовательскими данными в качестве аргумента.
Похожая ситуация будет с командой os.system, которая позволяет выполнять в операционной системе, команды переданные в качестве текстовой строки. Например:
import os
user_input = "foo && cat /etc/passwd" # value supplied by user
os.system("grep -R {} .".format(user_input))
Опасные команды
Прямое выполнение команд ОС в скриптах может привести к неприятным последствиям, если злоумышленник выполнит команды с привилегированными правами. Так, если к примеру у нас назначены права sudo на команду find то злоумышленник без труда сможет получить права root просто выполнив:
$ sudo find . -exec /bin/sh \; -quit
Подобных небезопасных команд довольно много и даже, если ваш скрипт не работает под sudo но у злоумышленника есть возможность выполнить команду ОС, которой назначены эти права, в итоге атакующий все-равно получит root.
Не доверяй пользователю
Завершая тему инъекций хотелось бы дать общие рекомендации по работе с пользовательским вводом. Прежде всего не доверяйте тем данным которые передает пользователь, не рассчитывайте на то, что он передаст все корректно. Вообще, лучшим решением является везде, где это возможно избегать ситуации, когда пользователь сам вводит какие-то значения, аргументы команд и уж тем более сами команды. В случае с веб приложениями можно воспользоваться различными визуальными компонентами: прокрутками, RadioButton, списками и другими элементами для того, чтобы пользователь мог указать нужные значения не осуществляя ввод.
Также по возможности для реализации функционала отдельных команд ОС лучше использовать встроенные библиотеки Python. Это позволит избежать прямого общения с ОС и необходимость использования консольных команд.
Но если ваш скрипт должен получать пользовательский ввод в текстовом виде, то необходимо использовать специальные функции для проверки. Это могут быть уже упомянутые validate(), shlex(). Также можно использовать регулярные выражения:
import re
…
Python_code = input()
Pattern = re.compile(‘re_command_pattern’)
If pattern.fullmatch(python_code):
# выполнение python_code
…
Еще можно использовать абстрактные синтаксические деревья, которые также позволяют проверить пользовательский ввод на корректность:
import ast
ast.literal_eval(string)
Не только инъекции
Помимо инъекций существуют также другие угрозы, и о некоторых из них мы поговорим далее. При запуске файла без абсолютного пути может использоваться небезопасное значение переменной PATH или запуск файла из другого каталога. Собственно это относится не только к Python, проблема в том, что если мы не указываем абсолютный путь к выполняемому файлу, то поиск этого файла будет осуществляться в тех каталогах, которые прописаны в переменной PATH. И при наличии доступа к файловой системе злоумышленник может поместить свой файл с таким же именем в каталог, который находится в PATH раньше, чем тот который требуется.
Так если наш файл находится в /usr/games, а злоумышленник разместит свой файл с таким же именем в /usr/local/bin, то при запуске без абсолютного пути выполнится его файл.
Также надо быть аккуратнее с запуском скриптов и установка обновлений из не доверенных каталогов. Так в каталоге Downloads может оказаться много разных файлов, не всегда безопасных, и установка такого пакета может привести к неприятным последствиям.
В целом не стоит забывать об использовании инструментов анализа исходного кода SAST. Существует множество как коммерческих, так и бесплатных решений. В качестве примера бесплатного решения можно привести утилиту Bandit.
Этот анализатор позволяет выявить в исходном коде небезопасные функции, уязвимые и устаревшие команды и многое другое.
Заключение
В этой статье мы рассмотрели основные моменты, связанные с безопасной разработкой на Python. Прежде всего важно помнить об инъекциях команд, не доверять пользовательскому вводу и регулярно проверять свой код с помощью инструментов SAST.
Эту статью я подготовил в преддверии старта курса DevSecOps от OTUS. По ссылке вы можете подробнее узнать о курсе, а также регистрируйтесь на бесплатный вебинар по теме: «Обеспечение безопасности в Docker‑контейнерах».