На одном проекте мы узнали об SQL-инъекции, когда логи начали показывать странные запросы к базе. Кто-то уже неделю тыкал в нашу форму авторизации. Код прошёл code review, прошел тесты, прошёл QA. Но никто не заметил, что строка 247 в auth.py — это открытая дверь.
Хорошо, что обошлось. Но этот урок я запомнил.
С тех пор я не полагаюсь только на людей. Люди устают, люди торопятся, люди пропускают детали. Поэтому SAST анализатор — это не опция, а база. Разберёмся, как он работает, что ловит и как внедрить так, чтобы не хотелось всё выбросить.
Что такое SAST и чем отличается от DAST
SAST — Static Application Security Testing. Анализирует исходный код без его запуска. Сканер смотрит на код, строит абстрактное синтаксическое дерево, граф потока данных и ищет паттерны, которые ведут к уязвимостям.
DAST — Dynamic Application Security Testing. Это когда ты уже запустил приложение и сканер тыкает в него снаружи, как злоумышленник. Находит уязвимости в рантайме, но не видит исходный код.
Простая аналогия: SAST — это когда архитектор смотрит на чертежи здания и говорит "здесь стена слишком тонкая, пройдут". DAST — это когда пожарный пытается выбить дверь и она действительно падает.
Оба подхода нужны. Но SAST работает раньше. На этапе коммита, а не деплоя.
Какие уязвимости ловит SAST анализатор
Зависит от языка и инструмента, но обычно это:
SQL-инъекции. Классика, которая не стареет.
# Плохо — склейка строки
query = f"SELECT * FROM users WHERE id = {user_input}"
# Хорошо — параметризованный запрос
cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,))
SAST анализатор увидит, что user_input приходит извне и попадает в SQL-запрос без санитизации. Это tainted data flow — отслеживание "грязных" данных от источника до опасной функции.
XSS — Cross-Site Scripting.
// Плохо — вставка без экранирования
element.innerHTML = userInput;
// Хорошо
element.textContent = userInput;
Тут сканер отслеживает, что пользовательский ввод попадает в innerHTML или аналогичный контекст.
Жёстко закодированные секреты.
# SAST найдёт это
API_KEY = "sk-live-4eC39HqLyjWDarjtT1zdp7dc"
DATABASE_PASSWORD = "admin123"
Регулярки ищут паттерны ключей, токенов, паролей. Да, иногда бывают false positives на тестовые данные. Но лучше проверить, чем слить продакшн-ключ.
Уязвимости десериализации.
# Опасно — pickle может выполнить произвольный код
pickle.loads(user_data)
# Безопаснее — json только для данных
json.loads(user_data)
В Python pickle с непроверенными данными — это RCE. SAST знает об этом.
Небезопасные конфигурации.
# SAST подскажет, что это плохо
security:
ssl:
enabled: false
cors:
allow-origin: "*"
Отключенный SSL, открытый CORS, дефолтные пароли — всё это ловится статическим анализом.
Как SAST анализатор работает под капотом
Коротко: парсинг → абстрактное синтаксическое дерево → граф потока управления → анализ.
Сначала код парсится в AST. Потом строится граф потока управления (CFG) — какие функции куда ведут, какие ветки возможны. Затем граф потока данных (DFG) — откуда приходят данные, куда уходят.
Если данные приходят из ненадёжного источника (user input, env variable, network) и попадают в опасный sink (SQL query, file write, command execution) без санитизации — это уязвимость.
Современные SAST-инструменты используют ещё и taint analysis — отслеживание "заражения" данных через все преобразования. Даже если user_input прошёл через три функции, сканер знает, что он всё ещё опасен.
Инструменты: что выбрать
Бесплатные и open-source:
- Semgrep — быстрый, кастомизируемый, поддерживает кучу языков. Пишешь правила в YAML.
- Bandit — для Python, простой и эффективный.
- ESLint с плагинами безопасности — для JavaScript/TypeScript.
- SpotBugs с FindSecBugs — для Java.
- SonarQube Community — комбайн, есть SAST-правила.
Коммерческие: Checkmarx, Veracode, Snyk, Fortify. Мощнее, но дорого.
GitLab CI имеет встроенный SAST. Настраивается в одну строку:
include:
- template: Security/SAST.gitlab-ci.yml
GitHub — через CodeQL и Dependabot.
Но есть нюанс. Инструменты сами по себе бесполезны, если их не внедрить правильно.
Внедрение в pipeline: как не сделать жизнь адом
Тут многие обжигаются. Подключили SAST, получили 2000 предупреждений, через неделю все их игнорируют. Знакомо?
Вот что работает по моему опыту.
Начинайте с критичных уязвимостей. Не пытайтесь исправить всё сразу. Настройте сканер на high и critical. Medium и low можно отложить.
Не ломайте сборку на первых порах. Пусть SAST просто репортит. Дайте команде время привыкнуть, разобрать backlog. Через месяц-два можно сделать так:
# Пример для GitLab CI
sast:
stage: test
script:
- /analyzer run
artifacts:
reports:
sast: gl-sast-report.json
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
allow_failure: false # Блокируем MR при критичных багах
- allow_failure: true # Для остальных веток — предупреждение
Отмечайте false positives. SAST не идеален. Если сканер ругается на безопасный код — пометьте это. Не игнорируйте молча, иначе потом пропустите реальную уязвимость.
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use
subprocess.run(["echo", user_provided_value]) # Здесь user_provided_value валидируется выше
Большинство инструментов поддерживают инлайн-аннотации для подавления ложных срабатываний.
Интегрируйте в code review. SAST-отчёт должен быть частью MR, а не письмом на почту, которое никто не читает. Инлайн-комментарии прямо в диффе — идеально.
Пример из практики: как мы нашли реальную дыру
На одном проекте мы подключили Semgrep к Python-коду. Одна из первых находок — использование eval() с пользовательским вводом.
# Код был таким
def calculate(expression):
return eval(expression) # "Удобно" для калькулятора
Разработчик думал, что это безопасно, потому что "expression приходит из формы и валидируется JavaScript-ом на фронте". Но JavaScript-валидацию можно обойти за секунду.
Semgrep подсветил это сразу. Исправили на:
import ast
def calculate(expression):
tree = ast.parse(expression, mode='eval')
# Проверяем, что в выражении только разрешённые операции
for node in ast.walk(tree):
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
ast.Num, ast.operator, ast.unaryop)):
raise ValueError("Invalid expression")
return eval(compile(tree, '<string>', 'eval'))
Не идеально, но уже не RCE. А лучше — использовать библиотеку для безопасных вычислений.
False positives и как с ними жить
SAST анализатор перестраховывается. Это его работа. Лучше десять раз предупредить, чем один раз пропустить.
Процент ложных срабатываний зависит от инструмента и кодовой базы. Bandit для Python даёт мало шума. Semgrep с дефолтными правилами — средне. Настроенный под проект — минимально.
Как уменьшить шум:
- Настройте правила под свой стек. Если не используете определённые библиотеки — отключите проверки для них.
- Исключите generated code, vendor-директории, тесты с моками.
- Прогоните на существующем коде и разметьте известные false positives.
SAST + AI: куда движется рынок
Традиционный SAST работает по правилам. Если уязвимость не описана правилом — она не будет найдена. AI-анализаторы меняют игру.
LLM может понять контекст. Видит, что переменная называется is_admin и проверяется в условии выше. Понимает бизнес-логику, а не только синтаксис.
Мы в Distiq как раз развиваем это направление. AI-бот анализирует MR/PR и оставляет комментарии не по шаблону "использована опасная функция", а с объяснением, почему именно здесь проблема и как её исправить.
Плюс AI понимает кросс-файловые связи. Функция определена в одном файле, используется в другом, данные текут через три модуля. Традиционный SAST теряется. LLM — нет.
Резюме
SAST анализатор — это не серебряная пуля, но и не бессмысленный шум. Это фильтр грубой очистки, который ловит очевидные дыры до того, как они попадут в продакшн.
Главное — внедрять постепенно. Не ломать сборку на старте. Разбирать backlog. Интегрировать в code review.
И помнить: SAST находит только известные паттерны. Неизвестные уязвимости, проблемы архитектуры, бизнес-логику — это всё ещё работа для людей. Но механические ошибки пусть ловит машина.
Кстати, если используете GitLab, GitHub или GitVerse — попробуйте Distiq. Бот делает AI code review, в том числе ищет уязвимости. Бесплатно для небольших команд, ставится за пару минут через webhook. Я рекомендую не потому, что работаю над ним, а потому что сам бы использовал.
