На одном проекте мы словили SQL-инъекцию в продакшене. Классика — параметр из запроса без санитизации ушёл в query. Код прошёл ревью у двух сеньоров. Никто не заметил. Потому что люди не замечают такое. Люди устают, люди торопятся, люди смотрят в телефон пока катится билд.
После того инцидента мы внедрили Semgrep. За неделю он нашёл ещё три подобных места. Теперь я не запускаю проект без SAST.
Что такое SAST и почему без него никак
Static Application Security Testing — это анализ кода без его запуска. Инструмент смотрит на исходники и ищет паттерны, которые могут привести к уязвимостям. В отличие от DAST, который долбит запущенное приложение запросами, SAST работает с текстом. Как линтер, только про безопасность.
Semgrep — один из инструментов SAST. С open-source версией. Написан на OCaml (да, это существует), работает быстро, не требует сборки проекта. В отличие от того же SonarQube, который надо разворачивать и кормить ресурсами, Semgrep — это бинарник. Установил, запустил, получил результат.
Главная фишка Semgrep — правила. Они читаемые. Их можно писать самому. Смотрите:
rules:
- id: sql-injection
patterns:
- pattern: cursor.execute($QUERY, ...)
- pattern-not: cursor.execute("...", ...)
message: "Possible SQL injection — query is not a string literal"
severity: ERROR
languages: [python]
Правило ловит вызовы cursor.execute, где запрос — не строковый литерал. То есть переменная. Значит, потенциально пользовательский ввод. Простейший случай, но его пропускают постоянно.
Типичные уязвимости, которые ловит Semgrep
SQL-инъекции
Самый дорогой класс уязвимостей. Не по частоте — по последствиям. Утечка базы, дамп пользователей, репутационные потери.
# Плохо — классическая инъекция
def get_user(username):
query = f"SELECT * FROM users WHERE username = '{username}'"
return db.execute(query)
# Хорошо — параметризованный запрос
def get_user(username):
query = "SELECT * FROM users WHERE username = ?"
return db.execute(query, (username,))
Semgrep найдёт первый вариант. И второй пометит как безопасный. Умеет различать f-strings от обычных строк, понимает конкатенацию, видит, где переменная пришла из запроса.
Hardcoded secrets
Пароли, ключи API, токены прямо в коде. Каждый второй проект этим страдает. Особенно если проект делали "на вчера" и деплой был ручной.
# Semgrep это найдёт
API_KEY = "sk-1234567890abcdef"
DATABASE_PASSWORD = "admin123"
# И это тоже
requests.get(url, auth=("admin", "super_secret"))
Правила Semgrep знают форматы ключей AWS, GitHub, Slack, Stripe и ещё сотни сервисов. Находит даже "закодированные" в base64 пароли. Спойлер: base64 — это не шифрование.
XSS и рендеринг пользовательского контента
Актуально для веб-приложений, которые возвращают HTML. Особенно если вы используете шаблонизаторы неправильно.
# Flask, опасно
@app.route('/profile')
def profile():
bio = request.args.get('bio')
return render_template_string(f"<div>{bio}</div>")
# Django, тоже опасно
def view(request):
data = request.GET.get('data')
return HttpResponse(mark_safe(data))
render_template_string с f-string и mark_safe — это почти гарантированный XSS. Semgrep видит, что переменная пришла из request, и орёт.
Command injection
Выполнение системных команд с пользовательским вводом. Одна из самых критичных уязвимостей — можно получить RCE.
import os
import subprocess
# Катастрофа
filename = request.args.get('file')
os.system(f"cat {filename}")
# Тоже плохо
subprocess.call("grep " + pattern + " file.txt", shell=True)
Если shell=True и в команде есть пользовательский ввод — это беда. Semgrep найдёт и os.system, и subprocess с опасными паттернами.
Path traversal
Чтение файлов за пределами разрешённой директории. Классика: пользователь передаёт ../../../etc/passwd.
# Уязвимый код
@app.route('/download')
def download():
filename = request.args.get('file')
with open(f"/var/files/{filename}") as f:
return f.read()
Semgrep проверит, есть ли санитизация пути. Нет? Будет замечание.
Интеграция в CI/CD pipeline
По-хорошему, SAST должен работать на каждый пул-реквест. Не после мерджа, не перед релизом, а именно на этапе ревью. Чтобы разработчик видел проблемы до того, как код попадёт в основную ветку.
Простейший вариант для GitLab CI:
semgrep:
image: returntocorp/semgrep
script:
- semgrep ci --config=auto
rules:
- if: $CI_MERGE_REQUEST_IID
allow_failure: false
Для GitHub Actions:
name: Semgrep
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: auto
config: auto — это набор правил, которые Semgrep выбирает автоматически по языкам в проекте. Для продакшена лучше явно указать правила:
config:
- p/security-audit
- p/secrets
- p/python
- p/javascript
Правила — это YAML-файлы. Можете писать свои под специфику проекта. Например, если у вас есть внутренняя библиотека для работы с БД, можно написать правило, которое проверяет, что все запросы идут через неё, а не через сырой SQL.
Отчёты и игнорирование ложных срабатываний
Любой SAST даёт false positives. Это неизбежность. Semgrep — не исключение. Где-то он увидит инъекцию там, где её нет. Где-то не распознает санитизацию.
Для таких случаев есть аннотации:
# nosemgrep: sql-injection
query = f"SELECT * FROM {table_name}" # table_name из конфига, не от пользователя
Или в отдельном файле .semgrepignore:
legacy/
migrations/
vendor/
Лучше не игнорировать файлы целиком, а подавлять конкретные правила. Иначе пропустите реальные проблемы.
Отчёты Semgrep можно выгружать в JSON или SARIF. SARIF — стандартный формат для security-отчётов, его понимают GitLab, GitHub, Azure DevOps. Отчёт можно залить в SonarQube или DefectDojo для агрегации.
Semgrep против конкурентов
SonarQube — классика. Мощный, но тяжёлый. Требует сервер, базу, настройку. Для маленькой команды — оверхед. Semgrep запускается за минуту.
Checkmarx — Enterprise-монстр. Дорогой, закрытый, с годовым контрактом. Если у вас миллион строк кода и отдельный security-отдел — ок. Для стартапа или mid-size — избыточно.
Snyk — хороший продукт, но триальный период заканчивается, и начинается боль. Semgrep имеет реально свободную версию с открытым кодом. Правила из Community Registry бесплатны.
CodeQL от GitHub — мощно, но сложно. Нужно строить базу данных кода, писать запросы на специальном языке. Semgrep — проще. Правила читаемые, можно написать за полчаса.
Практические рекомендации
Начинайте с готовых правил. Не пытайтесь сразу написать свои. Конфигурация auto или p/security-audit покроет 80% случаев. Остальные 20% — это специфика вашего проекта.
Добавляйте Semgrep в pre-commit хуки. Локально, до пуша. Быстрая проверка на критичные уязвимости занимает секунды. Да, это не заменит полный анализ в CI, но сэкономит время на исправление.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/returntocorp/semgrep
rev: v1.52.0
hooks:
- id: semgrep
args: ['--config', 'auto', '--error']
Смотрите на severity. ERROR — блокируйте мердж. WARNING — можно пропустить, но оставить комментарий. INFO — просто информация.
Обучайте команду. SAST-отчёт — не приговор, а подсказка. Разработчик должен понимать, почему код помечен как опасный. Иначе будет просто ставить # nosemgrep везде.
Внедряйте постепенно. Сначала на новые проекты, потом на существующие. На легаси Semgrep найдёт сотни проблем. Это демотивирует. Лучше включить его только на новых MR, а старый код проверять отдельно.
Если не хотите настраивать Semgrep и разбираться с правилами — есть готовые решения. Distiq, например, делает автоматический code review с проверкой безопасности. Он использует похожие подходы к анализу кода, но всё уже настроено за вас. Интегрируется в GitLab, GitHub или GitVerse за пару минут, находит уязвимости и оставляет комментарии прямо в MR. Я рекомендую попробовать, если нужно быстро закрыть вопрос с безопасностью без настройки инфраструктуры.
