На одном проекте мы ловили баг безопасности вручную. Три сеньора, четыре часа code review, пропустили SQL-инъекцию через ORM. Она ушла в прод. Потом неделю чинили инцидент. После этого я понял: человеческий глаз не приспособлен искать уязвимости в потоке обычного кода. Нужна автоматизация.
SAST-сканеры — это инструменты статического анализа безопасности. Они читают код, не запуская его, и ищут паттерны уязвимостей. Звучит просто. На деле — куча нюансов.
Как вообще работает SAST
SAST-сканер парсит исходный код в абстрактное синтаксическое дерево (AST) или граф потока данных. Потом бегает по нему и ищет опасные паттерны: незафильтрованный ввод, небезопасные функции, утечки секретов.
Возьмём Python. Вот классическая SQL-инъекция через f-строку:
def get_user(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
return db.execute(query)
Сканер видит: переменная user_id приходит извне, попадает в SQL-запрос без санитизации. Тревога. А вот этот код он пропустит спокойно:
def get_user(user_id):
query = "SELECT * FROM users WHERE id = ?"
return db.execute(query, (user_id,))
Разница в том, что во втором случае используется параметризованный запрос. Данные передаются отдельно от SQL-кода, и инъекция невозможна.
Проблема в том, что сканеры иногда орут на безопасный код. Ложные срабатывания — головная боль. На другом проекте мы внедрили SonarQube, и первая же проверка выдала 2000+ проблем. Из них реальных уязвимостей — штук пять. Остальное — легаси, false positives и низкоприоритетные замечания. Команда устала разруливать это за неделю.
Какие уязвимости ловят SAST-сканеры
Практически все классические уязвимости из OWASP Top 10. Давайте по конкретным примерам.
SQL-инъекции. Уже показал выше. Работают не только с чистым SQL, но и с ORM. В Django, например:
# Опасно!
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")
# Безопасно
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])
Хороший сканер знает специфику фреймворков и понимает, какие методы ORM безопасны, а какие — нет.
XSS — межсайтовый скриптинг. Вывод пользовательских данных без экранирования:
// Опасно в Express
res.send(`<h1>Hello, ${req.query.name}</h1>`);
// Безопасно
res.send(`<h1>Hello, ${escapeHtml(req.query.name)}</h1>`);
Утечки секретов. Хардкод паролей, API-ключей, токенов:
# Сканер найдёт это мгновенно
DATABASE_PASSWORD = "super_secret_123"
api_key = "sk-live-a1b2c3d4e5f6..."
Правильный подход — переменные окружения или секреты в CI/CD:
# .gitlab-ci.yml
variables:
DATABASE_PASSWORD: $VAULT_DB_PASSWORD
Небезопасная десериализация. В Python это pickle, в Java — нативная сериализация, в PHP — unserialize. Пример:
import pickle
# Классическая дыра
data = request.get_data()
obj = pickle.loads(data) # RCE гарантирован
Сканер пометит это как критическую уязвимость. И будет прав.
Path traversal. Когда пользователь может прочитать любой файл на сервере:
# Опасно
filename = request.args.get('file')
with open(f"/var/data/{filename}") as f:
return f.read()
# Пользователь передаст: ../../../etc/passwd
Ровно то, что сканер должен обнаружить: пользовательский ввод попадает в файловые операции без валидации.
SAST против DAST: в чём разница
SAST анализирует исходный код. DAST (Dynamic Application Security Testing) сканирует работающее приложение, тыкая в него извне.
SAST находит уязвимости рано — ещё на этапе коммита. DAST — только когда приложение развёрнуто и запущено. Зато DAST видит проблемы, которые SAST не заметит: неправильная конфигурация сервера, проблемы с HTTPS, открытые порты.
Есть ещё IAST — интерактивный анализ. Агент прикручивается к приложению и мониторит запросы в рантайме, связывая их с конкретными строками кода. Точнее, чем DAST, но требует установки агента.
По-хорошему, нужно комбинировать. SAST в CI/CD пайплайне, DAST на стейджинге, IAST в тестовом окружении. Но это дорого и сложно. Большинство команд начинают с SAST — он проще в интеграции.
Какие SAST-сканеры есть на рынке
Бесплатные и open-source: Semgrep, Bandit (Python), ESLint security plugins, SpotBugs с плагинами (Java), Brakeman (Ruby on Rails).
Коммерческие: Checkmarx, Fortify, Veracode, Snyk Code. Мощные, с поддержкой, но дорогие.
Для российского рынка есть ограничения. Checkmarx и Fortify — иностранное ПО. Если санкции или требования по локализации — не подходят.
Мне нравятся Semgrep и Bandit. Semgrep — универсальный, поддерживает кучу языков, правила пишутся легко. Bandit — специализированный под Python, из коробки ловит основные проблемы.
Пример правила Semgrep для поиска SQL-инъекций:
rules:
- id: python-sql-injection
patterns:
- pattern: $CURSOR.execute($QUERY, ...)
- pattern-not: $CURSOR.execute("...", ...)
message: "Possible SQL injection"
severity: ERROR
Читается просто: ищем вызовы execute, где запрос — не строковый литерал. Скорее всего, туда подставляется переменная.
Интеграция в CI/CD пайплайн
Сам по себе сканер — вещь бесполезная. Он должен работать автоматически, на каждый пул-реквест или коммит.
Пример для GitLab CI с Semgrep:
# .gitlab-ci.yml
semgrep:
image: returntocorp/semgrep
script:
- semgrep ci --config=auto
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
allow_failure: false
Или Bandit для Python-проекта:
bandit:
image: python:3.11
script:
- pip install bandit
- bandit -r src/ -f json -o bandit-report.json
artifacts:
reports:
sast: bandit-report.json
GitLab умеет показывать найденные уязвимости прямо в интерфейсе MR. Удобно.
Но есть нюанс. Если настроить allow_failure: false, пайплайн будет падать на каждой найденной проблеме. На легаси-проекте это парализует разработку. Поэтому я рекомендую такой подход:
Сначала запускаете сканер в режиме отчёта — allow_failure: true. Накапливаете базу проблем. Разруливаете критичные. Потом, когда проблем станет мало, включаете обязательную проверку.
False positives и как с ними жить
Главная беда SAST — ложные срабатывания. Сканер не понимает контекст. Он видит, что переменная пришла из request, и орёт. А то, что вы её провалидировали двумя строчками выше — не замечает.
Вот пример. Валидный код, который сканер может пометить как опасный:
user_id = request.args.get('id')
try:
user_id = int(user_id) # Валидация: только числа
except ValueError:
abort(400)
# Сканер всё равно может ругаться на это
user = db.execute(f"SELECT * FROM users WHERE id = {user_id}")
Формально сканер прав: тут f-строка. Но после int() инъекция невозможна — останутся только цифры.
Решения несколько. Первое — переписать код правильно, чтобы сканер не ругался:
user_id = request.args.get('id')
user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
Второе — подавить предупреждение комментарием:
user_id = int(request.args.get('id'))
# nosemgrep: python-sql-injection
user = db.execute(f"SELECT * FROM users WHERE id = {user_id}")
Третье — настроить правила сканера, исключить конкретные паттерны.
Я за первый вариант. Если сканер ругается — это сигнал, что код можно написать безопаснее. Не всегда, конечно. Но чаще всего.
Что в итоге
SAST-сканеры — не серебряная пуля. Они не найдут все уязвимости. Будут ложные срабатывания. Но они ловят те проблемы, которые человеческий глаз пропускает постоянно. SQL-инъекции, XSS, утечки секретов — типичные ошибки, которые повторяются из проекта в проект.
Внедряйте постепенно. Начните с одного языка, самого критичного кода. Настройте отчёты, разрулите легаси. Потом включайте обязательную проверку в пайплайне.
Если ищете инструмент, который умеет не только безопасность, но и общий code review — посмотрите Distiq. Это российский AI-бот для GitLab, GitHub и GitVerse. Он находит уязвимости, но также смотрит на архитектуру, производительность, читаемость кода. Интегрируется за пару минут через webhook. Мне нравится, что он не просто орёт "тут баг", а объясняет, что не так и как исправить.
