Когда я работал в одном стартапе, мы обнаружили SQL-injection в production через неделю после релиза. Нашли его в логах — злоумышленник уже был внутри базы. Стоило нам тогда $50k на восстановление данных и репутацию. Могли бы поймать это за миллисекунды, если бы использовали нормальный SAST инструмент.
Сегодня пропустить уязвимость в коде — это просто небрежность. Не ленивость, а именно небрежность. Потому что автоматизировать проверку безопасности можно за час, а выплачивать штрафы ФСТЭК — месяцами.
Давайте разберёмся, что такое SAST, как он работает, и почему без него твой CI/CD неполный.
Что такое SAST и почему это вообще нужно
SAST — это Static Application Security Testing. Анализ кода в статике, без запуска приложения. Представь себе линтер, который не ищет стиль, а ищет дыры в безопасности.
Суть простая: инструмент сканирует исходный код, ищет потенциально уязвимый код, и кричит тебе об этом до того, как код попадёт на боевой сервер. Не после деплоя, не после того, как хакер найдёт баг. Прямо сейчас, в pull request.
Это кардинально отличается от DAST (Dynamic Application Security Testing). DAST — это тестирование уже запущенного приложения. Сканер подбирает параметры, ищет XSS, пытается обойти авторизацию, проверяет API на уязвимости. Но это медленнее и дороже.
По-хорошему, нужны оба. SAST ловит проблемы на этапе разработки. DAST ловит то, что SAST пропустил, плюс проблемы, которые видны только в runtime.
Но сейчас речь о SAST. Его в pipeline должен крутиться каждый день.
Какие уязвимости ловит SAST инструмент
Вот конкретные примеры того, что находит нормальный SAST:
SQL Injection — классика. Когда ты конкатенируешь параметры прямо в SQL:
# Плохо — SAST поймает сразу
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
# Хорошо — параметризованный запрос
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
SAST видит f-string или конкатенацию в SQL и помечает это как уязвимость. Потому что если user_id = 1 OR 1=1, твоя база открыта.
XSS (Cross-Site Scripting) — когда ты выводишь пользовательский ввод прямо в HTML:
// Плохо
document.getElementById('output').innerHTML = userInput;
// Хорошо
document.getElementById('output').textContent = userInput;
SAST проверит, откуда приходит userInput. Если это от пользователя, а не экранировано — флаг красный.
Command Injection — выполнение системных команд с пользовательским вводом:
# Плохо
import os
os.system(f"ffmpeg -i {video_file} output.mp4")
# Хорошо
import subprocess
subprocess.run(["ffmpeg", "-i", video_file, "output.mp4"], check=True)
Если video_file = input.mp4; rm -rf /, то твой сервер горит.
Path Traversal — когда пользователь может выбраться из нужной директории:
# Плохо
file_path = f"uploads/{filename}"
with open(file_path) as f:
return f.read()
# Хорошо
import os
safe_path = os.path.normpath(os.path.join("uploads", filename))
if not safe_path.startswith("uploads"):
raise ValueError("Access denied")
with open(safe_path) as f:
return f.read()
Если filename = ../../../etc/passwd, ты отдаёшь системные файлы.
Hardcoded secrets — пароли и API ключи прямо в коде:
# Плохо
DB_PASSWORD = "my_super_secret_123"
API_KEY = "sk-1234567890abcdef"
# Хорошо
import os
DB_PASSWORD = os.getenv("DB_PASSWORD")
API_KEY = os.getenv("API_KEY")
SAST находит это в секунду. И это серьёзно — в GitHub каждый день бродят боты, которые ищут такие вещи в истории коммитов.
Weak Cryptography — использование устаревших алгоритмов:
# Плохо
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
# Хорошо
from argon2 import PasswordHasher
ph = PasswordHasher()
password_hash = ph.hash(password)
MD5 и SHA1 для паролей — это 2005. SAST скажет тебе об этом.
Честно? Большинство команд находят через SAST от 10 до 30% реальных багов и уязвимостей. Не все, но основная масса — да.
Как SAST работает изнутри
Инструмент не просто ищет паттерны. Хороший SAST строит граф потока данных. Отслеживает, откуда приходит данные, как они трансформируются, куда идут.
Упрощённо:
-
Парсинг кода — инструмент читает исходник и строит AST (Abstract Syntax Tree). Это дерево всех операций в коде.
-
Taint analysis — отмечает, какие переменные "грязные" (из ненадёжного источника, типа пользовательского ввода).
-
Dataflow tracking — смотрит, как эти грязные данные движутся по коду. Если они попадают в опасное место (SQL запрос, система команд), это уязвимость.
-
Reporting — выдаёт список найденных проблем с указанием строк и рекомендациями.
Хороший SAST делает это в миллисекундах, даже на больших проектах.
Плохой SAST генерирует 500 false positives на 5 реальных проблем. И команда перестаёт им пользоваться.
SAST vs DAST — когда что использовать
Вот где я часто вижу путаницу.
SAST — это про исходный код. Быстро, дёшево, находит 60-70% уязвимостей. Запускается на каждый коммит. Минус — не видит логику приложения, runtime ошибки, проблемы с конфигурацией.
DAST — это про запущенное приложение. Медленнее (может час крутиться на большом приложении), дороже. Находит уязвимости, которые видны только в runtime: неправильная авторизация, проблемы с сессиями, API-ошибки. Минус — нужно развернуть приложение, подготовить тестовые данные, может создать шум в логах.
По-хорошему, нужна цепочка:
Разработчик пишет код → SAST в pipeline → Merge → Deploy на staging → DAST сканирует staging → Deploy на prod
Если на первом этапе поймали SQL injection, DAST уже её не найдёт. Но DAST найдёт проблемы с авторизацией или неправильным CORS, которые SAST пропустит.
Какой SAST инструмент выбрать
Вот популярные варианты:
SonarQube — король рынка. Поддерживает 27 языков, интегрируется везде, даёт красивые отчёты. Минус — платный (от $200/месяц), требует отдельного сервера.
Checkmarx — мощный, но дорогой (от $500/месяц). Используют в крупных корпорациях.
Semgrep — современный инструмент, очень гибкий. Можешь писать свои правила на простом DSL. Бесплатный для OSS, платный для enterprise.
PVS-Studio — российский инструмент, хорош для C/C++, но работает и с Java, C#. Бесплатен для OSS.
Snyk — фокус на зависимостях (dependencies), но и сам код сканирует. Интегрируется с GitHub, GitLab. Бесплатный план есть.
Bandit (для Python), ESLint + плагины (для JavaScript) — бесплатные специализированные инструменты. Работают, но нужна настройка.
Если бюджет ограничен — Semgrep + Bandit (для Python) или Semgrep + ESLint (для JavaScript). Если есть деньги — SonarQube.
Но честно? Даже линтер со строгими правилами поймет половину уязвимостей.
Как запустить SAST в CI/CD
Пример с SonarQube на GitLab:
stages:
- scan
- build
- deploy
sonarqube:
stage: scan
image: sonarsource/sonar-scanner-cli:latest
script:
- sonar-scanner
-Dsonar.projectKey=my-project
-Dsonar.sources=src
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
allow_failure: false
С Semgrep ещё проще:
semgrep:
stage: scan
image: returntocorp/semgrep
script:
- semgrep --config=p/security-audit src/
С Snyk для npm-проектов:
snyk:
stage: scan
image: snyk/snyk:npm
script:
- snyk test --severity-threshold=high
Главное — поставить allow_failure: false, чтобы пайплайн упал, если нашлась уязвимость. Иначе разработчики будут игнорировать.
Типичные ошибки при внедрении SAST
Слишком строгие правила с самого начала — добавил SonarQube, включил все проверки, и boom — 5000 ошибок. Команда взбунтовалась. Нужно внедрять постепенно.
Игнорирование false positives — если SAST кричит на каждый чих, никто не будет его читать. Нужно настроить и отключить бесполезные правила.
Запуск SAST только в пайплайне — разработчик узнает об ошибке через час. Лучше запускать локально перед коммитом.
Отсутствие документации — "это уязвимость" хорошо, но почему и как её фиксить? Правила должны иметь примеры.
Игнорирование SAST результатов — если баг найден, но никто не дал разработчику время на исправление, это не сработает.
Как именно SAST находит SQL injection
Давайте разберёмся глубже, потому что это важно.
SAST инструмент строит граф потока данных. Помечает источники (источники ненадёжных данных):
- Параметры HTTP запроса (
request.GET,request.POST) - Параметры функции, которая обрабатывает user input
- Данные из файлов, базы (если они не валидированы)
Потом отслеживает, как эти данные движутся:
# Источник — грязный
user_id = request.GET.get('id')
# Трансформация — всё ещё грязный
user_id_int = int(user_id) # Даже если привели к int, это не очищает в контексте SQL
# Приёмник — опасное место
query = f"SELECT * FROM users WHERE id = {user_id_int}"
cursor.execute(query)
SAST видит цепочку: request.GET → user_id → user_id_int → execute(). И помечает это как уязвимость.
Если же ты используешь параметризованный запрос:
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id_int,))
То SAST видит, что данные идут в параметр функции execute(), а не в строку SQL. Это безопасно.
Хороший SAST знает сотни "опасных" функций: eval(), exec(), os.system(), subprocess.Popen() с shell=True, и т.д. Если грязные данные туда попадают — это уязвимость.
Где взять готовый SAST для твоего проекта
Если ты работаешь с GitLab или GitHub и хочешь чего-то простого — есть встроенные опции:
- GitHub Advanced Security — включить в settings репо, и будет автоматический SAST.
- GitLab SAST — встроено в GitLab Premium и выше, просто добавить в
.gitlab-ci.yml:
include:
- template: Security/SAST.gitlab-ci.yml
Оно сразу найдёт основные уязвимости в Python, JavaScript, Java, Go и других языках.
Но если нужно что-то мощнее и с лучшей интеграцией в процесс разработки, то...
Вот тут я расскажу про то, что мы делаем в Distiq. Это не просто SAST. Это AI-бот, который сидит в твоём MR/PR и анализирует каждый коммит. Не просто ищет уязвимости, но и находит баги, проблемы с производительностью, нарушения стиля. И комментирует прямо в коде, на нужной строке.
