Перейти к содержимому

Semgrep SSRF настройка + Burp Active Scan в одном CI/CD-пайплайне

Semgrep SSRF настройка + Burp Active Scan в одном CI/CD-пайплайне
Время чтения: 13 мин.

На аудите Python-сервиса, который генерировал превью ссылок по пользовательскому URL, Semgrep поймал передачу user_url в requests.get() ещё на этапе коммита — за секунды. Burp Active Scan на staging-окружении через пару часов нашёл ту же точку входа, но показал кое-что поинтереснее: сервис послушно шёл за цепочкой 302-редиректов и добирался до 169.254.169.254 — облачного metadata-сервиса AWS. Один дефект, два инструмента, два слоя информации. Первый увидел опасный паттерн в коде. Второй подтвердил, что паттерн реально эксплуатируется в рабочем окружении.

Дальше покажу, как собрать оба сканера в одном CI/CD-пайплайне (автоматизированной цепочке сборки, тестирования и деплоя кода) и почему запуск только одного из них — не «простота», а слепое пятно.

Зачем ловить SSRF на двух уровнях: статический анализ SSRF уязвимостей и DAST

SSRF (Server-Side Request Forgery — подделка серверного запроса) — уязвимость, при которой злоумышленник заставляет сервер отправить HTTP-запрос на адрес, который выбрал не разработчик, а атакующий. По классификации OWASP Top 10:2021 это отдельная категория — A10, Server-Side Request Forgery: «SSRF flaws occur whenever a web application is fetching a remote resource without validating the user-supplied URL». Сервер действует от своего имени: у него есть доступ к внутренней сети, к соседним микросервисам и — в облаке — к metadata-эндпоинтам с временными credentials.

Зачем это атакующему? Три основных сценария:

  • Через SSRF сервер обращается к metadata-сервису (AWS: 169.254.169.254, GCP: metadata.google.internal) и отдаёт атакующему временные ключи роли инстанса. В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — её идентификаторы) это техника T1552.005 — Cloud Instance Metadata API, тактика Credential Access. Проще говоря: атакующий через ваш сервер крадёт ключи от облака.
  • SSRF в API, доступном из интернета — это T1190, Exploit Public-Facing Application, тактика Initial Access. По данным Mandiant M-Trends 2025, эксплойты остаются самым распространённым вектором первоначального проникновения — 38% расследований.
  • Через SSRF атакующий проверяет, какие порты и сервисы доступны из серверного сегмента — то, что извне скрыто файрволом. Фактически ваш сервер становится сканером внутренней сети.

Теперь к главному: почему один инструмент не покрывает все эти сценарии.

SAST (Static Application Security Testing — статический анализ кода без его запуска) сканирует исходники и ищет паттерны. Semgrep находит конструкции вида «пользовательский ввод попадает в requests.get() без валидации». Быстро, дёшево, срабатывает при каждом коммите. Но SAST не знает, как приложение ведёт себя в рантайме: следует ли HTTP-клиент за редиректами, какие DNS-ответы получает сервер, как ведут себя прокси между клиентом и целью.

DAST (Dynamic Application Security Testing — динамический анализ запущенного приложения) работает как внешний сканер. Burp Active Scan отправляет реальные HTTP-запросы к задеплоенному приложению и наблюдает за ответами. Он обнаруживает то, что SAST в принципе увидеть не может:

  • DNS rebinding — домен при проверке резолвится в безопасный IP, а при реальном запросе — в 127.0.0.1.
  • Redirect chains — фильтр пропустил исходный URL, но после цепочки 302-редиректов сервер ушёл на внутренний адрес.
  • URL parsing bugs — библиотека валидации и библиотека запросов по-разному разбирают один и тот же URL. Пример из документации Semgrep: https://semgrep.dev.attacker.com проходит проверку на домен semgrep.dev, но реально обращается к attacker.com.

При этом DAST не видит исходный код и сканирует только те эндпоинты, которые обнаружил краулер.

Критерий Semgrep (SAST) Burp Active Scan (DAST)
Скорость Секунды Минуты — часы
Что сканирует Исходный код Запущенное приложение
Прямой SSRF (user input в http request) Да Да
DNS rebinding Нет Да
Redirect chains Нет Да
Blind SSRF (out-of-band) Нет Да (через Collaborator)
Cross-file data flow Нет (OSS) / Частично (Pro) Не применимо
Покрытие эндпоинтов Весь код в репозитории Только обнаруженные краулером
Когда НЕ подходит Runtime-зависимые баги, сложные redirect-цепочки API без UI, эндпоинты без seed URLs
Стоимость Бесплатно (OSS) $449+/год (Professional)

Shift-left безопасность SSRF — не «SAST вместо DAST», а «SAST раньше, DAST потом, оба в пайплайне».

Semgrep правила для SSRF: от стандартных до кастомных

—config auto: что есть из коробки и почему этого мало

Semgrep — open-source инструмент, который ищет паттерны в коде по правилам в формате YAML (текстовый формат конфигурации с отступами, похожий на структурированный список). Команда semgrep --config auto . запускает сканирование текущего каталога стандартным набором из открытого реестра Semgrep Registry. Для Python в нём есть SSRF-правила: они ловят случаи, когда результат request.args.get() (получение параметра из HTTP-запроса во Flask) передаётся напрямую в requests.get() или urllib.request.urlopen(). Для JavaScript покрывают аналогичные потоки через fetch() и axios.

Проблема: стандартные правила рассчитаны на типовой код. В реальном проекте URL может приходить не из query-параметра, а из тела JSON-запроса, из HTTP-заголовка или из очереди сообщений. Стандартное правило этого не увидит.

Ещё одно ограничение: бесплатная Semgrep OSS не поддерживает cross-file taint analysis — отслеживание потока данных между файлами. Если пользовательский ввод приходит в views.py, а используется для HTTP-запроса в services.py — OSS-движок это пропустит. Cross-file анализ доступен в Semgrep Pro Engine (платная подписка). По данным appsec.guide (Trail of Bits), Semgrep наиболее эффективен, когда баг можно обнаружить «by looking at single files» и «via intraprocedural analysis». Для SSRF это часто так: паттерн «user_input попадает в http_request» обычно живёт в одном файле-обработчике.

Вывод: для реального проекта пишем кастомные правила под свои паттерны.

Кастомное правило: пошаговая сборка

Допустим, Flask-сервис принимает URL в теле JSON-запроса и передаёт его в requests.get(). Стандартные правила из реестра это могут не покрыть, потому что ищут request.args, а не request.json.

Предпосылки: установленный Semgrep (через pip install semgrep или Docker-образ returntocorp/semgrep), текстовый редактор.

Создайте файл .semgrep/ssrf-json-body.yaml:

rules:
  - id: ssrf-from-json-body
    patterns:
      - pattern: requests.$METHOD($URL, ...)
      - pattern-either:
          - pattern: $URL = request.json.get(...)
          - pattern: $URL = request.json[...]
    message: "URL из тела запроса в requests без валидации (CWE-918)"
    severity: ERROR
    languages: [python]

Разберём каждое поле — ошибка в YAML превращает правило в мёртвый груз без предупреждений:

  • id — уникальное имя правила, появляется в отчёте и CI-логах.
  • patterns — список условий, которые должны совпасть одновременно (логическое И).
  • pattern: requests.$METHOD($URL, ...) — ищет вызов любого метода библиотеки requests (get, post, put) с переменной $URL в первом аргументе. Метапеременная $METHOD подхватывает любое имя метода, $URL — любое выражение в позиции первого аргумента.
  • pattern-either — логическое ИЛИ: $URL мог прийти из request.json.get("key") или из прямого обращения request.json["key"].
  • severity: ERROR — при срабатывании Semgrep вернёт exit code 1, если запущен с флагом --error. Это блокирует пайплайн.
  • languages: [python] — правило применяется только к .py-файлам.

Как проверить, что правило работает: создайте тестовый Flask-файл с роутом, который получает target_url из request.json.get() и передаёт его в requests.get(url) без валидации. Запустите semgrep --config .semgrep/ssrf-json-body.yaml test_ssrf.py. Ожидаемый результат — строка с именем файла, номером строки и текстом: URL из тела запроса в requests без валидации ([CWE-918](https://cwe.mitre.org/data/definitions/918.html)). Если вывод пустой — проверяйте отступы в YAML (Semgrep требует ровно 2 пробела, не табуляции) и совпадение languages с расширением файла.

Три типичных ошибки при первой настройке Semgrep SSRF правил:

  1. YAML-отступы. Одна табуляция вместо пробелов — и правило молча игнорируется. Semgrep не выдаёт ошибку, просто ничего не находит. Это бесит.
  2. Слишком широкий паттерн. Правило pattern: requests.get(...) без привязки к источнику данных ловит все HTTP-вызовы, включая тесты и мониторинг. Добавьте paths: { exclude: ["tests/", "monitoring/"] } или используйте инлайн-комментарий # nosemgrep: ssrf-from-json-body для осознанных исключений.
  3. Метапеременные в нижнем регистре. $url вместо $URL — Semgrep не подхватит, ошибки не покажет. Просто тишина в выводе.

Burp Active Scan настройка для автоматизации поиска SSRF

Burp Suite — инструмент динамического тестирования веб-приложений от PortSwigger. В ручном режиме пентестеры перехватывают трафик через прокси, модифицируют запросы и анализируют ответы. Для CI/CD нужна Burp Suite автоматизация сканирования — и тут два варианта.

Burp Suite Enterprise Edition — продукт под автоматизированное сканирование. REST API для запуска сканов, управления конфигурацией, выгрузки результатов. Интеграция с GitLab CI, Jenkins, GitHub Actions — через HTTP-вызовы. По данным PortSwigger, «Burp Suite DAST integrates directly into your CI pipeline, automating security scans with each build».

Burp Suite Professional + headless-обёртка — вариант для команд с лицензией Professional ($449/год), но без бюджета на Enterprise. Поднимает Burp как headless-сервис с REST-интерфейсом. Работает, но требует виртуальный дисплей (Xvfb) на Linux-раннере и заметно менее стабильна. Если выбираете этот путь — готовьтесь дебажить Xvfb-сессии. На первой настройке это отнимает больше времени, чем написание самих правил.

Предпосылки для интеграции: лицензия Burp (Professional или Enterprise); staging-среда с задеплоенным приложением — Burp сканирует работающий сервис, не исходный код; сетевой доступ от CI-раннера до staging по HTTP/HTTPS.

Ключевая штука Burp для SSRF — Burp Collaborator. Это внешний сервер, который фиксирует входящие DNS-запросы и HTTP-обращения. Burp подставляет в параметры URL вида http://xxxxx.burpcollaborator.net и ждёт: если целевой сервер обратился к этому домену — SSRF эксплуатируется. Это единственный надёжный способ находить blind SSRF — сценарии, где ответ сервера не содержит результат запроса, но сам факт обращения подтверждается через out-of-band канал (внешний сервер, регистрирующий входящие запросы).

На практике Burp Active Scan обнаруживает SSRF-сценарии, невидимые для Semgrep: redirect chains (фильтр пропустил исходный URL, но при follow redirect сервер ушёл на 169.254.169.254), DNS rebinding, partial URL injection (пользователь передаёт только path, сервер собирает полный URL из шаблона), protocol confusion (file://, gopher:// в параметре, ожидающем HTTP).

Ограничение: Burp сканирует только те эндпоинты, о которых знает. API-сервисы без UI не дают краулеру зацепок. Решение: передавайте Burp OpenAPI/Swagger-спецификацию или список seed URLs при запуске скана.

AppSec pipeline SSRF: собираем SAST и DAST в GitLab CI

Архитектура SSRF в CI/CD пайплайне — три стадии: Semgrep запускается на каждый merge request (быстро, блокирует очевидные дефекты), код деплоится на staging, Burp Active Scan проверяет staging (медленнее, но глубже). Пример на GitLab CI — логика переносится на Jenkins и GitHub Actions.

Делай раз, делай два, делай три

Шаг 1. Подготовка (что нужно до начала): — Файл с Semgrep-правилами в репозитории: .semgrep/ssrf-json-body.yaml — Рабочий staging с URL, доступным CI-раннеру: https://staging.yourapp.internal — Burp Enterprise API endpoint (или headless Professional) — CI/CD-переменные в GitLab: BURP_API_URL, BURP_API_KEY, STAGING_URL

Шаг 2. Добавьте .gitlab-ci.yml в корень репозитория:

stages: [sast, deploy_staging, dast]
semgrep_ssrf:
  stage: sast
  image: returntocorp/semgrep
  script: [semgrep --config .semgrep/ --config auto --error .]
  allow_failure: false
burp_dast:
  stage: dast
  needs: [deploy_staging]
  script: [./scripts/run-burp-scan.sh "$STAGING_URL"]
  allow_failure: true

Что происходит на каждом шаге и как понять, что получилось:

stages: [sast, deploy_staging, dast] — три стадии, выполняются последовательно. Если sast упал — deploy_staging и dast не запустятся.

semgrep_ssrf использует Docker-образ returntocorp/semgrep. Флаг --config .semgrep/ подключает кастомные правила, --config auto добавляет стандартные из реестра, --error заставляет вернуть exit code 1 при любой находке. Если пайплайн прошёл стадию без ошибок — в CI-логах будет строка No findings. Если нашёл уязвимость — стадия упадёт с описанием находки и номером строки.

allow_failure: false — SAST-находка блокирует merge request. Код не попадёт в staging.

burp_dast стартует только после успешного деплоя (needs: [deploy_staging]). Скрипт run-burp-scan.sh вызывает API Burp Enterprise: отправляет POST-запрос с URL staging-среды через curl. Конкретный формат зависит от версии API — сверяйтесь с документацией PortSwigger для вашей версии.

allow_failure: true — DAST-находка не блокирует MR, а уходит в дашборд. Иначе часовые сканы парализуют разработку.

Шаг 3. Проверка работоспособности. Сделайте коммит с намеренно уязвимым кодом: Flask-роут, который передаёт request.json["url"] в requests.get(). Ожидаемый результат: stage sast упадёт с описанием находки. Если прошёл без ошибок — проверьте путь к правилам в --config и структуру YAML-файла (отступы, languages). После успешного деплоя проверьте логи stage dast: Burp должен вернуть ID скана, результаты появятся в дашборде Enterprise или в JSON-артефакте.

Про stage deploy_staging: в YAML он опущен, потому что зависит от инфраструктуры (Docker Compose, Kubernetes, Terraform). Суть: между sast и dast приложение должно быть задеплоено и доступно CI-раннеру по HTTP.

Что ломается при DAST SAST интеграции в CI/CD

Четыре проблемы, которые стабильно возникают при настройке AppSec pipeline SSRF на реальных проектах.

False positives превращают SAST в шум. Правило ловит каждый requests.get(url) — включая тестовые файлы, health-check обёртки и мониторинговые скрипты. По данным исследования, опубликованного в arxiv (Automating Static Code Analysis Through CI/CD Pipeline Integration), одна из главных причин, по которой команды перестают использовать SAST — «high rates of false positives and unhelpful warning messages». Решение: добавляйте paths: { exclude: [...] } в правило и ведите список осознанных исключений через # nosemgrep. Каждое исключение — задокументированное решение, не «отключить, чтобы не мешало».

Burp не видит все эндпоинты. API-сервисы без фронтенда не дают краулеру зацепок. Без OpenAPI-спецификации или seed URLs Burp проверит только корневой URL и несколько обнаруженных ссылок. Передавайте Swagger-файл при запуске скана — покрытие вырастет кратно.

Тайминг парализует разработку. Burp Active Scan может работать от 15 минут до часа на среднем приложении. Блокировать merge request на такой срок — неприемлемо. Два подхода: запускать DAST с allow_failure: true и настроить уведомления при находках, или запускать DAST по расписанию (раз в сутки / раз в неделю), а не на каждый коммит.

Разные форматы отчётов. Semgrep выдаёт SARIF или JSON, Burp Enterprise — собственный JSON. Чтобы свести результаты в одну панель, нужен агрегатор: GitLab Security Dashboard (встроен в Ultimate), DefectDojo (open-source) или скрипт, который парсит оба формата и создаёт issues в трекере. Та же архитектура описана в исследовании на arxiv: контроллер-скрипт, который «aggregates SAST tool outputs and integrates them into developers’ familiar issue-tracking software». На практике это Python-скрипт на 100–200 строк с дедупликацией по файлу и строке — не космическая инженерия, но без него два отчёта так и останутся двумя артефактами, которые никто не читает.

Большинство команд, которые приходят за «AppSec-пайплайном», на самом деле хотят не пайплайн, а волшебную кнопку: нажал — безопасно. Ставят один инструмент, получают 200 алертов на первом прогоне, понимают, что разбирать их некому, и через месяц тихо отключают.

Проблема не в инструменте. Связка SAST + DAST работает не потому, что «два лучше одного». Она работает, потому что заставляет ответить на неудобные вопросы: что именно блокирует релиз, а что — информационное уведомление? Кто смотрит отчёт DAST — разработчик или AppSec-инженер? Что происходит, когда Burp находит blind SSRF в пятницу вечером?

Без ответов на это даже три сканера дадут только ложное чувство защищённости. А навык сборки такого пайплайна — одна из самых быстрых точек входа из IT в ИБ. Компании ищут не теоретиков, а людей, которые умеют написать .gitlab-ci.yml с двумя stage, отладить YAML-правило и объяснить разработчику, почему его requests.get(url) — не «просто HTTP-вызов», а потенциальная дверь во внутреннюю сеть. На курсе IB Basics эту связку SAST + DAST разбирают на практике — для тех, кто перешёл из IT в ИБ и хочет структуру за пару месяцев.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.