Как защитить веб-приложение, которое вы не до конца понимаете

Где-то есть веб-приложение, за которое вы отвечаете, и никто не может, не отводя глаз, сказать вам, безопасно ли выставлять его форму входа в интернет. Может, приложение досталось вам по наследству. Может, коллега «наваял» его за выходные. Может, ему десять лет, а исходной команды давно нет. Всё это одна и та же задача: вам нужно защитить веб-приложение, внутренности которого вы не до конца понимаете. Вы не можете пройтись по каждому маршруту, каждой проверке авторизации, каждой зависимости и каждому обращению к базе и лично за них поручиться. Переписывание требует кварталов, которых у вас нет, а оставить всё открытым неприемлемо. Об этой середине и статья.

Если считать имеющееся приложение чёрным ящиком, защищать его надо снаружи, слой за слоем. Ни один инструмент не защитит его целиком, но горстка дополняющих друг друга, сложенных вместе, покрывает большинство очевидных способов, которыми его вскроют. Три основных слоя, примерно в том порядке, в котором их стоит добавлять:

СлойЧто делает
Сканирование секретовНаходит учётные данные, утёкшие в репозиторий и его историю.
Фильтрация на периметреWAF перед приложением, фильтрующий явно злонамеренный трафик.
Защита от ботовCAPTCHA или обнаружение ботов по оценке на формах, которые должны оставаться публичными.

Все три слоя выше стоят на периметре — границе между вашим приложением и внешним миром. Сканирование секретов не даёт учётным данным утечь из репозитория, WAF фильтрует входящий злонамеренный трафик, защита от ботов бросает вызов нечеловеческим посетителям на формах, которые должны оставаться публичными. Ни одному из них не нужно понимать внутренности приложения, чтобы работать, — именно поэтому они так полезны, когда эти внутренности под вопросом.

У OWASP для такого приёма есть название: виртуальное латание — защитные слои, снижающие уязвимость, пока нижележащий код всё ещё под вопросом. Ни один из них не заменяет нормального переписывания или нормального аудита. Все они покупают вам время.

Периметру статья уделяет большую часть времени, но мы также затронем немного того, что можно сделать за периметром — посмотрев вместо этого внутрь: на библиотеки, которые приложение реально запускает, на его исходный код и на его поведение в рантайме. Сканирование зависимостей находит библиотеки с известными уязвимостями, которые приложение уже тащит с собой, а SAST и DAST в конце статьи ловят шаблоны на уровне кода, которых не видит никакой периметровый слой.

У большинства этих слоёв есть надёжный самостоятельно разворачиваемый вариант, работающий на любом хостинге: gitleaks для секретов, ModSecurity для WAF, OSV-Scanner для зависимостей, Semgrep для SAST, ZAP для DAST. Они остаются основными на протяжении всей статьи. Что касается управляемых облачных эквивалентов, где вы обмениваете часть работы по настройке на настроенный вендором набор правил и его данные об угрозах, — их предлагает каждое крупное облако. В качестве конкретного ориентира, когда управляемый пример помогает, мы будем брать Google Cloud Platform: Cloud Armor для WAF, Artifact Analysis для сканирования контейнеров, reCAPTCHA Enterprise и Adaptive Protection для защиты от ботов. Выбор одного облака делает эти примеры конкретными, а не отмашкой; у AWS, Azure и Cloudflare есть эквиваленты, которые мы упомянем коротко, но разбирать глубоко не будем.

Три категории неизвестного

Прежде чем брать инструменты, стоит назвать, от чего вы на самом деле защищаетесь, потому что от этого зависит, какой слой важнее всего. Неизвестное распадается на три группы.

Что в коде и в репозитории. Вы не знаете, какие маршруты существуют, — легаси-приложения растут органически, и там наверняка есть админские эндпоинты, о которых никто не упоминал. Вы не знаете и того, что код на самом деле делает, — либо потому что не успеваете его прочитать (легаси), либо потому что его изначально никто внимательно не читал (сгенерирован ИИ). И вы не знаете, что уже утекло в историю git. DAST зондирует работающее приложение снаружи, чтобы составить карту того, что реально доступно; SAST даёт неполную, но полезную карту рискованных шаблонов кода; сканирование секретов ловит учётные данные, забытые в старых коммитах.

Что работает рядом с кодом. Вы не знаете, какие зависимости актуальны, — у десятилетнего приложения десятки библиотек с публичными CVE (Common Vulnerabilities and Exposures — публичный каталог известных уязвимостей, у каждой стандартизованный идентификатор вида CVE-2024-12345). Сканирование зависимостей — самый выгодный по отдаче слой для этого.

Что происходит с приложением в продакшене. Вы не знаете, кто вас атакует и как, — журнал аудита WAF расскажет вам это в течение суток после запуска. И вы не знаете, какая доля вашего трафика — боты, а на публичных формах регистрации, входа и сброса пароля это часто большинство. Защита от ботов это показывает и позволяет на это реагировать.

Ни один инструмент не решает всё это. Каждый из трёх слоёв ниже адресует один или несколько пунктов; сканирование зависимостей (о нём после слоёв) и SAST/DAST (в конце) наращивают глубину, когда основа уже на месте.

Сканирование секретов

Репозитории теряют учётные данные — легаси потому, что росли органически годами, «наваянные» потому, что LLM жизнерадостно прописывают ключи в код «для удобства», а диф никто не смотрел. Ключи API, ключи доступа AWS, пароли к базам, токены внутренних сервисов — они оказываются в коммитах, в конфигах, в старых тестовых фикстурах, в документации, в сгенерированных ИИ примерах, которые незаметно подставляют настоящий ключ вместо заглушки. А поскольку git хранит историю навсегда, даже коммиты, «удалившие» секрет, всё ещё его содержат.

Быстрее всего отдача от приложения-чёрного-ящика приходит от сканирования полной истории git всех задействованных репозиториев, ротации всего найденного и последующего удержания сканера в CI, чтобы новые утечки ловились.

Инструменты

  • gitleaks — быстрый, на Go, с отличными правилами по умолчанию, покрывающими большинство провайдеров (AWS, GCP, Azure, Stripe, Twilio, токены GitHub, приватные ключи, JWT). Запускайте по полной истории, а не только по HEAD.
  • trufflehog — похожее обнаружение, но добавляет шаг проверки: он тестирует, действительно ли найденный секрет живой (например, делает вызов sts:GetCallerIdentity для ключей AWS). Вывод с более высоким сигналом, дополнительная задержка того стоит.
  • git-secrets — ориентирован на AWS, легковесный, полезен как pre-commit-хук.
  • GitHub Secret Scanning — бесплатно для публичных репозиториев, Advanced Security для приватных. Сотрудничает с провайдерами, так что утёкшие токены автоматически отзываются.

Запускаем сканирование и читаем вывод

gitleaks можно установить несколькими способами — готовый бинарник, Homebrew, go install или через Docker. Здесь мы возьмём Docker для удобства: на хост ничего ставить не надо, одна и та же команда работает на любой машине с демоном Docker, а раннеры CI получают тот же вызов, что и ваш ноутбук.

Я только что прогнал это по репозиторию, в котором живёт эта статья:

docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest \
    detect --source=/repo --log-opts="--all" --redact --verbose

--log-opts="--all" проходит по каждому коммиту каждой ветки, а не только по HEAD; --redact маскирует значение секрета в выводе, чтобы сам отчёт не стал новой утечкой; --verbose печатает полный блок по каждой находке вместо просто сводного счётчика.

gitleaks выдаёт по одной находке на каждую потенциальную строку-секрет, которую опознаёт — один и тот же набор полей для любого совпадения, независимо от того, какое правило сработало. Типичная находка выглядит так:

Finding:     STRIPE_SECRET_KEY=REDACTED
Secret:      REDACTED
RuleID:      stripe-access-token
Entropy:     4.175736
File:        blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx
Line:        225
Commit:      49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa
Fingerprint: 49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:stripe-access-token:225

Пять полей, которые имеют значение:

ПолеО чём говорит
RuleIDКакое правило обнаружения сработало (stripe-access-token, aws-access-token, generic-api-key, private-key и т. д.). Полный набор правил по умолчанию лежит в config/gitleaks.toml в репозитории gitleaks — каждое правило со своей регуляркой, описанием и любыми фильтрами по ключевым словам.
File + LineГде совпадение.
CommitКакой коммит его внёс — не обязательно самый последний. gitleaks находит его там, где он впервые появляется в истории.
EntropyЭнтропия Шеннона совпавшей строки, в битах на символ. Выше — значит выглядит более случайно, а это обычно значит, что вероятнее это настоящий секрет. Правила для общих ключей API используют энтропию как основной сигнал; правила под конкретных провайдеров (Stripe, AWS) совпадают по префиксу и в ней не нуждаются.
FingerprintУстойчивый идентификатор, которым вы подавите эту находку, если она окажется допустимой. Об этом ниже.

Что энтропия на самом деле измеряет

Энтропия Шеннона измеряет, насколько непредсказуем в среднем каждый символ строки. У строки, где каждый символ с равной вероятностью мог быть любым из N значений, максимально возможная энтропия: log₂(N) бит на символ. У строки, где один символ встречается гораздо чаще других или где следующий символ предсказуем из предыдущего, энтропия ниже.

Для сканирования секретов вопрос, на который энтропия пытается ответить, таков: выглядит ли эта строка так, будто её сгенерировал случайный источник, или так, будто её написал человек? Настоящие секреты — ключи API, хеши, токены в base64 — спроектированы быть непредсказуемыми, поэтому они толкают энтропию к максимуму алфавита. Имена, английские слова, даты, строки версий, пути к файлам и шаблонный текст сидят гораздо ниже. Энтропия — это дешёвый тест, отделяющий «это накатил CSPRNG» от «это набрал человек или сгенерировал код по шаблону».

Грубая шкала:

  • ниже ~3.0 — похоже на английский текст, идентификаторы, строки версий или повторяющуюся структуру. Что-то вроде password123, [email protected], v1.2.3-beta, пути к файлам.
  • ~4.0 — близко к максимуму для чистого hex (log₂(16) = 4). Хеши MD5/SHA, токены в hex, UUID.
  • ~5.0 — типично для случайных буквенно-цифровых токенов. Ключи API с фиксированным префиксом и высокоэнтропийным случайным хвостом.
  • ~5.95 — потолок для полностью случайного 62-символьного алфавита [a-zA-Z0-9]. Длинные секреты в духе base64 или чисто буквенно-цифровые.

Наша находка выше набрала 4.175736 — заметно выше порога gitleaks по умолчанию ≈3.5 для общих правил и ровно в той полосе, куда попадают настоящие ключи провайдеров. Фиксированный префикс (что-то вроде sk_live_) немного тянет среднюю энтропию вниз; случайный хвост толкает её обратно вверх. Эта комбинация — ровно то, как выглядит настоящий ключ API.

Разбор находок

Когда вы реально прогоните gitleaks по настоящему репозиторию, находки будут трёх категорий — и одна и та же реакция не подходит всем трём:

  1. Настоящая утечка. Учётные данные, закоммиченные случайно: файл .env, конфиг с прописанным ключом API, тестовая фикстура с настоящим токеном. Ротируйте немедленно. Считайте, что учётные данные скомпрометированы, даже если репозиторий приватный. Бывшие подрядчики и старые бэкапы ноутбуков — не теоретическая угроза. Затем решите, переписывать ли историю (только для случаев высокой чувствительности вроде продакшен-паролей к базе или ключей подписи — переписывание разрушительно, делайте это осознанно).
  2. Намеренное включение. Строка действительно совпадает с шаблоном секрета, но должна быть в репозитории: документация, цитирующая контролируемые атакующим учётные данные как доказательство в разборе инцидента, тестовые фикстуры с намеренно фальшивыми, но правдоподобно выглядящими значениями, примеры конфигов. В репозитории, где живёт эта статья, есть ровно такой случай — разбор атаки на цепочку поставок цитирует прописанные в злонамеренном проекте ключи Stripe и API, чтобы показать, чем пользовался атакующий. gitleaks справедливо их помечает, но это не настоящие учётные данные, так что ротировать нечего.
  3. Ложное срабатывание. Шаблон совпал, но строка на самом деле не секрет: случайный base64 в хеше, длинное значение вида UUID, слово из сообщения коммита с высокой энтропией. Реже с правилами под конкретных провайдеров, чаще с общими.

Для категорий 2 и 3 лечение — файл .gitleaksignore в корне репозитория со списком идентификаторов (поле Fingerprint из таблицы выше) тех находок, которые вы просмотрели и одобрили:

# Намеренно: секреты, приведённые как доказательство в разборе атаки на цепочку поставок
49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:stripe-access-token:225
49e4ca23a5ee6d1ef6b1a566a580a4086fbb84aa:blog/src/content/blog/anatomy-of-a-developer-targeted-supply-chain-attack.mdx:generic-api-key:193

gitleaks будет пропускать именно эти находки при последующих сканированиях. Комментируйте щедро — будущему вам нужно знать, была ли каждая запись одобрена потому, что она намеренная, или потому, что это ложное срабатывание, и комментарий — единственная долговечная запись об этом решении.

Остановить следующую утечку в момент коммита

Когда историческое сканирование чисто (или отпечатки взяты и подавлены), вплетите gitleaks в:

  • CI — блокирует сборку при любых новых находках. Тот же вызов Docker работает в любом раннере CI.
  • Pre-commit-хук — ловит утечки ещё до того, как они попадут в ваш локальный репозиторий. Для этого есть официальный режим gitleaks protect, либо используйте фреймворк pre-commit с хуком gitleaks.

Вместе это означает, что новая утечка теперь требует активного обхода обоих слоёв — сделать это случайно куда труднее, чем намеренно.

Фильтрация на периметре с WAF

Web Application Firewall — это обратный прокси, который проверяет HTTP(S)-запросы по набору правил. Он решает, разрешить, залогировать, заблокировать или бросить вызов каждому запросу до того, как тот дойдёт до вашего приложения. WAF полезен для:

  • Перехвата очевидных полезных нагрузок для классов атак: SQL-инъекция, XSS, локальное/удалённое включение файлов, инъекция команд, обход путей.
  • Блокировки известных плохих путей (/wp-admin/, .git/config, .env).
  • Ограничения частоты, блокировки по репутации IP, гео-фильтрации.
  • Покупки времени, когда в зависимости, которую вы не можете обновить сегодня, выходит CVE.

Он бесполезен против сломанной бизнес-логики, сломанной авторизации или плохого проектирования сессий. Это компенсирующая мера, а не лечение.

К WAF есть два операционных пути. Управляемый WAF запускает ваш облачный провайдер; самостоятельно развёрнутый WAF — это тот, который вы запускаете сами. Набор правил обнаружения под обоими по сути один и тот же — открытый движок ModSecurity v3 в паре с OWASP Core Rule Set (CRS) или переименованная вендором версия того же. CRS — это примерно 200 поддерживаемых сообществом правил обнаружения, покрывающих SQL-инъекции, XSS, RCE, обход путей и остальной OWASP Top 10. Он использует оценку аномальности — правила добавляют к счёту, и запрос блокируется только тогда, когда сумма пересекает порог, — что делает настройку мягче, чем блокировка по каждому правилу.

Так что выбор между управляемым и самостоятельным в основном операционный, а не про качество обнаружения. По обоим путям работает по сути один движок и одни правила.

Управляемый: Cloud Armor

Управляемый WAF почти всегда правильная точка старта: меньше инфраструктуры в эксплуатации, быстрее развернуть (вечер, а не неделя), нет движка, который надо обновлять и настраивать. Выбирайте это первым, если у вас нет конкретной причины не делать этого.

Родной для GCP вариант — Google Cloud Armor. Он подключается к HTTP(S)-балансировщику GCP, а его предварительно настроенные группы правил (sqli-v33-stable, xss-v33-stable, lfi-v33-stable, rce-v33-stable и т. д.) выведены напрямую из OWASP CRS — того же набора правил, который вы установили бы руками с ModSecurity.

Настраивать Cloud Armor можно через консоль GCP, CLI gcloud, REST API или — как мы покажем ниже — декларативно в Terraform. Модель одна и та же независимо от интерфейса: политика безопасности Cloud Armor — это список правил с условиями совпадения и действиями (allow, deny(403), rate_based_ban). Минимальная политика, включающая предварительно настроенные группы правил CRS и разумное действие по умолчанию, выглядит так:

resource "google_compute_security_policy" "app" {
  name = "app-waf"

  rule {
    action   = "deny(403)"
    priority = 1000
    match {
      expr { expression = "evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 2})" }
    }
    description = "Block SQL injection"
  }

  rule {
    action   = "deny(403)"
    priority = 1001
    match {
      expr { expression = "evaluatePreconfiguredWaf('xss-v33-stable', {'sensitivity': 2})" }
    }
    description = "Block XSS"
  }

  rule {
    action   = "allow"
    priority = 2147483647
    match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } }
    description = "Default allow"
  }
}

Есть один режим, о котором стоит упомянуть заранее: режим предпросмотра Cloud Armor. С ним включённым правила вычисляются как обычно и логируют каждое совпадение в Cloud Logging — но трафик на самом деле не отклоняют. Это управляемый эквивалент запуска ModSecurity в режиме только обнаружения, и именно так вы безопасно включаете совершенно новую политику на реальном продакшен-трафике, не рискуя случайно заблокировать свой платёжный колбэк в три часа ночи. Порядок выкатки тот же, что и для самостоятельного варианта: неделя в предпросмотре, настройка по тому, что сработало, потом выводите правила из предпросмотра по одному.

Помимо правил WAF Cloud Armor включает или сочетается с несколькими смежными возможностями: защита от DDoS (всегда включена), ML-слой адаптивной защиты, который обнаруживает объёмные аномалии и автоматически предлагает правила, и управление ботами. Последние два — платные надстройки, а не бесплатные базовые функции.

О компромиссах, которые надо знать. Язык пользовательских правил Cloud Armor — подмножество CEL, менее выразительное, чем полный синтаксис SecRule у ModSecurity, так что очень замысловатые пользовательские правила иногда нельзя выразить напрямую. Версию CRS выбирает Google, а не вы, так что остаться на конкретной ревизии не получится. И сверху есть плата за запрос вдобавок к исходящему трафику балансировщика.

У других облачных провайдеров есть свои эквиваленты. AWS WAF подключается к ALB/CloudFront/API Gateway со своим синтаксисом правил плюс управляемые группы правил; Azure Front Door / Application Gateway WAF тоже на основе CRS; Cloudflare WAF архитектурно другой — это обратный прокси-CDN, а не фильтр, подключённый к балансировщику, так что вы направляете DNS на Cloudflare, а ваш origin прячется за их сетью. Все три включают управляемые группы правил на основе CRS и эквивалент режима только обнаружения.

Самостоятельно: ModSecurity + NGINX

Самостоятельный путь — правильный выбор, когда вам нужен полный контроль над пользовательскими правилами, вы не в облаке с хорошим управляемым предложением, стоимость за запрос важна при ваших объёмах или ограничения комплаенса делают устройство, которым вы управляете сами, проще для обоснования, чем сервис вендора.

Вы запускаете ModSecurity v3 напрямую как динамический модуль NGINX, настроенный правилами OWASP CRS. Форма архитектуры такова:

Интернет  →  NGINX + ModSecurity  →  приложение выше по потоку

Вот как расставлены три части:

  • NGINX — обратный прокси. Он терминирует TLS, принимает входящие HTTP-запросы и — когда их ничто не блокирует — проксирует их приложению выше по потоку на приватный порт.
  • libmodsecurity — движок вычисления правил. Это библиотека на C++, отдельная от самого NGINX, без собственного сетевого кода: она просто принимает запрос на вход и возвращает вердикт.
  • Коннектор ModSecurity для NGINX — динамический модуль (файл .so), который связывает эти два. Он встраивается в конвейер обработки запросов NGINX и для каждого входящего запроса передаёт URL, заголовки и тело в libmodsecurity на вычисление.

Все три части работают внутри одного процесса NGINX. Механизм — загрузчик динамических модулей NGINX: и коннектор, и libmodsecurity в итоге загружаются в каждый рабочий процесс NGINX при старте, без отдельного демона ModSecurity, без IPC, без сокета между ними.

Когда приходит запрос, NGINX вызывает коннектор, тот вызывает libmodsecurity — всё внутри общей памяти процесса. Вот почему интеграция добавляет лишь несколько миллисекунд задержки, а не стоимость обхода до внепроцессного шлюза безопасности. И вот почему версия NGINX у модуля-коннектора должна в точности совпадать с версией NGINX, в которую он загружается: коннектор компилируется под внутренний ABI NGINX, который между версиями не стабилен.

Для каждого запроса NGINX передаёт URL, заголовки и тело в libmodsecurity (через коннектор). libmodsecurity прогоняет их через загруженные правила — OWASP CRS плюс любые пользовательские, — накапливает оценку аномальности и возвращает действие: allow, log или deny. Если вердикт deny, NGINX возвращает настроенный ответ об ошибке (обычно 403) и вверх по потоку не пересылает. Иначе запрос идёт как обычно.

Когда WAF на месте, убедитесь, что всё это верно:

  • Приложение не должно быть доступно напрямую. Если у него всё ещё есть публичный IP, атакующие обходят WAF целиком.
  • TLS терминируется на WAF. ModSecurity не может проверить то, что не может расшифровать.
  • WAF теперь единая точка отказа — запустите хотя бы два экземпляра за балансировщиком, если аптайм важен.
  • Объём журнала аудита подскочит. Планируйте хранилище.

Форма установки на Debian/Ubuntu (сокращённо):

# libmodsecurity
git clone --depth 1 -b v3/master https://github.com/owasp-modsecurity/ModSecurity
cd ModSecurity && git submodule init && git submodule update
./build.sh && ./configure && make -j$(nproc) && make install

# модуль-коннектор для NGINX (версия должна совпадать с вашей версией NGINX)
git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx
cd nginx-${NGINX_VERSION}
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules && cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/

# OWASP Core Rule Set
git clone --depth 1 https://github.com/coreruleset/coreruleset /etc/nginx/modsec/crs
cp /etc/nginx/modsec/crs/crs-setup.conf.example /etc/nginx/modsec/crs/crs-setup.conf

Затем в nginx.conf:

load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    server {
        listen 443 ssl;
        server_name legacy-app.example.com;
        ssl_certificate     /etc/ssl/certs/app.crt;
        ssl_certificate_key /etc/ssl/private/app.key;

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

Сначала выкатывайте в режиме только обнаружения. Поставьте SecRuleEngine DetectionOnly хотя бы на неделю. Читайте журнал аудита. Вы ищете ложные срабатывания (правила CRS, срабатывающие на легитимном трафике — редакторы, админ-панели, загрузки файлов) и неожиданные истинные срабатывания (атаки, которые уже идут по вам). CRS использует оценку аномальности — правила добавляют к счёту, и запрос блокируется только когда сумма пересекает порог, — что делает настройку мягче, чем блокировка по каждому правилу.

Когда найдёте ложное срабатывание, сначала используйте SecRuleRemoveById с областью до конкретного location, во вторую очередь понижайте уровень паранойи на пути, и лишь в крайнем случае отключайте правило глобально. Только после того, как логи станут достаточно тихими, чтобы быть осмысленными, переключайте SecRuleEngine On.

Для пользовательских виртуальных заплаток — скажем, вы знаете, что /api/report?format=X уязвим к инъекции команд, — можно отбрасывать на периметре всё, что вне списка разрешённых:

SecRule REQUEST_URI "@beginsWith /api/report" \
    "id:1000001,phase:1,chain,deny,status:400,\
     msg:'virtual patch: /api/report format allowlist'"
    SecRule ARGS:format "!@rx ^(pdf|csv|json)$"

Защита от ботов и CAPTCHA

Остальные слои этого набора почти не касаются класса угроз, который почти наверняка настигнет публичное легаси-приложение: оппортунистические злоупотребления ботами на эндпоинтах, которые обязаны быть неаутентифицированными — спам-регистрации, прогоны подстановки украденных учётных данных по вашей форме входа, массовый скрапинг, автоматизированные злоупотребления купонами, потоки сбросов пароля, вызывающие рассылку писем в масштабе.

Собственная аутентификация приложения здесь не помогает — формы регистрации, входа и сброса пароля обязаны быть доступны кому угодно. Ограничение частоты в WAF помогает, но распределённый ботнет, делающий по одному вежливо выглядящему запросу с каждого IP, выглядит идентично волне законных регистраций. Слой, который реально отличает людей от ботов, — это система обнаружения ботов или CAPTCHA.

Защита от ботов — это скорее спектр, чем единственный инструмент:

  1. Пассивные классификаторы — ML-модели, оценивающие каждый запрос по сигналам (отпечаток TLS, порядок заголовков, тайминги, поведенческая телеметрия) без видимого пользователю вызова. Наименьшее трение, лучше всего работает против объёмных злоупотреблений.
  2. CAPTCHA на основе оценки — классификатор плюс эскалация к видимому вызову только тогда, когда оценка низкая. Гибрид, который использует большинство современных систем.
  3. Всегда видимая CAPTCHA — классическое «нажмите на все светофоры». Высокое трение, сейчас обычно неверный выбор по умолчанию, но всё ещё полезно для конкретных высокоценных эндпоинтов.

Три подраздела ниже покрывают две из этих категорий спектра — CAPTCHA на основе оценки (Turnstile, reCAPTCHA Enterprise) и пассивный классификатор (Cloud Armor Adaptive Protection, Cloudflare Bot Management). Они скорее дополняют друг друга, чем являются альтернативами:

ПодразделКатегория спектраНа стороне клиента?Лучше всего для
Cloudflare TurnstileCAPTCHA по оценкеДа — JS-виджет на каждой формеБыстрые победы на формах регистрации/входа
reCAPTCHA EnterpriseCAPTCHA по оценкеДа — JS-виджет на каждой формеБолее глубокая интеграция с GCP, применение на периметре через Cloud Armor
Только классификаторПассивный классификаторНет — только серверные сигналыОбъёмные злоупотребления, трафик, куда вы не можете или не хотите добавлять виджет

Определяющее различие — надо ли добавлять виджет. И Turnstile, и reCAPTCHA Enterprise требуют JS-сниппет на каждой форме: виджет собирает сигналы уровня браузера (тайминги, поведенческую телеметрию, JS-вызовы) и отправляет токен, который проверяет ваш сервер. Инструменты только-классификатор оценивают каждый запрос, используя лишь то, что сеть и так видит: отпечаток TLS, порядок HTTP-заголовков, репутацию IP, объёмные аномалии. Встраивать нечего, код приложения менять не надо. Они естественно складываются: классификатор покрывает весь входящий трафик как одеяло; CAPTCHA целится именно в высокоценные формы.

Начнём с самого простого в настройке — Cloudflare Turnstile — а затем посмотрим на более глубокую родную для GCP комбинацию reCAPTCHA Enterprise плюс Cloud Armor Adaptive Protection для команд, которым нужно больше.

Cloudflare Turnstile (самый простой путь)

Если сегодня вы сделаете только одно — пусть это будет оно. Cloudflare Turnstile бесплатен, невидим по умолчанию, дружелюбен к приватности (без поведенческого снятия отпечатков, как это делает reCAPTCHA) и совместим «на вставку» с API reCAPTCHA v2 — так что позже вы сможете миграцию сделать без переписывания кода форм, если вырастете из него.

Настройка честно занимает вечер, и вам не нужно держать остальное приложение на Cloudflare — подробности установки смотрите в документации по началу работы с Turnstile. Настройка не требует ни изменений в DNS, ни Terraform, ни интеграции с вашим WAF.

Turnstile перестаёт быть достаточным, когда вам нужен более тонкий контроль — оценки по каждому запросу, на которые можно реагировать (а не просто пройдено/не пройдено), применение на балансировщике вместо кода приложения или правила тоньше, чем «вызов пройден / не пройден». Именно этот пробел закрывает комбинация reCAPTCHA Enterprise + Cloud Armor ниже.

reCAPTCHA Enterprise (более глубокая интеграция с GCP)

reCAPTCHA Enterprise — управляемый сервис обнаружения ботов от Google, потомок классической reCAPTCHA, перестроенный как API на основе оценки, а не вызов «пройдено/не пройдено». Каждый запрос получает оценку от 0.0 (почти наверняка бот) до 1.0 (почти наверняка человек) плюс список причин (AUTOMATION, UNEXPECTED_USAGE_PATTERNS, LOW_CONFIDENCE и т. д.). Что с этим делать, решаете вы.

Две вещи делают его естественным выбором для набора на базе GCP:

  1. Невидим по умолчанию. В отличие от старого интерфейса reCAPTCHA v2 «нажмите на все светофоры», Enterprise обычно работает молча в фоне. Пользователи не видят трения, если оценка не низкая, — а тогда вы можете подняться до видимого вызова.
  2. Он интегрируется с Cloud Armor. Cloud Armor может вычислять токен reCAPTCHA как часть выражения правила политики безопасности, так что применять оценки ботов можно на балансировщике, а не в приложении:
rule {
  action   = "deny(403)"
  priority = 500
  match {
    expr {
      expression = "token.recaptcha_action_token.valid && token.recaptcha_action_token.score < 0.5"
    }
  }
  description = "Block low-score bot traffic to sensitive endpoints"
}

Интеграция означает, что код вашего приложения встраивает JS-виджет reCAPTCHA в формы, отправляет полученный токен с каждым запросом, а Cloud Armor делает проверку оценки прежде, чем запрос дойдёт до бэкенда. Самому приложению не нужно знать, доверенный ли запрос.

Есть и собственно WAF-режим в Cloud Armor, называемый действиями управления ботами (redirect, googleRecaptcha), который выдаёт вызов reCAPTCHA прямо на периметре — полезно для сценария «подозрительный скачок трафика, бросаем вызов всем, пока не спадёт».

Google свернул reCAPTCHA Enterprise в более широкую платформу Cloud Fraud Defense, добавляющую классификацию ИИ-агентов, агентский движок политик и вызовы, устойчивые к ИИ. Существующие интеграции reCAPTCHA Enterprise продолжают работать без изменений — Fraud Defense расширяет, а не заменяет, без необходимости миграции и без изменения цен. Стоит следить, если трафик ИИ-агентов станет значимой долей того, что приходит на ваши формы.

Только классификатор (без интеграции на стороне клиента)

Более быстрый слой — без виджета, без встраивания в форму, без изменений в приложении — это чистый классификатор, который вычисляет запросы на периметре по сигналам, которые сеть и так видит: отпечаток TLS (JA3/JA4), порядок и регистр HTTP-заголовков, тайминги запросов, репутация IP, объёмные аномалии. Модель — это ML-классификатор бот/человек; запрос либо проходит, либо нет.

  • Cloud Armor Adaptive Protection — родной для GCP вариант. Это ML-слой внутри Cloud Armor, который следит за трафиком к каждому из ваших бэкенд-сервисов, обнаруживает объёмные атаки на L7 и шаблоны подстановки учётных данных / скрапинга, не совпадающие ни с одним отдельным правилом WAF, и автоматически генерирует кандидатное правило Cloud Armor, которое вы можете развернуть (или дать ему развернуть само), чтобы заблокировать нарушающий трафик. Непрерывно, интеграции с приложением не требует, видимого пользователю трения нет. Минус: он основан на шаблонах аномалий трафика, так что объёмные злоупотребления ловит гораздо лучше, чем медленные и тихие атаки от одного упорного действующего лица.
  • Cloudflare Bot Management — эквивалент на Cloudflare: ML-классификатор на каждом запросе, оценки от 1 (бот) до 99 (человек), настраиваемые правила по оценке. Сидит на том же слое, что и Cloudflare WAF.
  • Коммерческие платформы для изощрённых злоупотреблений: DataDome, HUMAN Security (ранее PerimeterX), Kasada. Они сочетают серверные классификаторы с клиентскими JS-зондами и являются правильным уровнем, когда вас целенаправленно атакуют (перекупка билетов, захват аккаунтов в масштабе, скрапинг цен конкурентом).

Правильная структура для большинства приложений — сначала классификатор, CAPTCHA как страховка: пусть пассивная модель молча обрабатывает большинство трафика, а к видимому вызову эскалируйте только те запросы, в которых классификатор не уверен. reCAPTCHA Enterprise делает это нативно; сочетание Adaptive Protection (объёмный слой) с reCAPTCHA Enterprise (оценка на запрос, способность к эскалации) даёт вам и периметровый фильтр, и запасной вызов.

Другие альтернативы

  • hCaptcha — замена reCAPTCHA v2, похожий интерфейс, больше внимания к приватности, платит сайтам за решённые вызовы. Использовался Cloudflare до того, как они построили Turnstile.
  • reCAPTCHA v2 / v3 — бесплатная классическая версия (не Enterprise). Всё ещё широко используется, но всё чаще избегается по причинам доступности и приватности, и у неё нет интеграции с Cloud Armor.
  • Arkose Labs — коммерческий, корпоративного уровня, игровые вызовы, которые очень трудно решать через фермы «решение как сервис». Дорого. Берите это только если ваше приложение целенаправленно атакуют изощрённые и упорные операторы ботов.

CAPTCHA — быстрый и относительно дешёвый механизм отфильтровать основную массу оппортунистического трафика ботов: большая часть автоматизированных злоупотреблений ненаправленна, сдаётся в момент, когда натыкается на вызов, а любое продакшен-приложение с формой регистрации без неё получает нетривиальную долю регистраций от ботов. Но это не полное решение: упорный атакующий может платить сервисам решения CAPTCHA примерно $1–3 за 1000 вызовов, чего достаточно, чтобы отпугнуть оппортунистические злоупотребления, но не целевую кампанию, а видимые вызовы добавляют трение, вредят конверсии и по-настоящему враждебны части пользователей с точки зрения доступности.

Так что они необходимы, но недостаточны. Практический подход:

  • Ограничьте их высокоценными, склонными к злоупотреблениям эндпоинтами — регистрация, вход, сброс пароля, контактные формы, использование купонов, всё, где приложение отправляет письмо или создаёт ресурс. Не на страницах только для чтения.
  • Интеграция на основе оценки (reCAPTCHA Enterprise, Turnstile) несравнимо лучше классических вызовов «пройдено/не пройдено» для UX — большинство настоящих пользователей вызов вообще не видят.
  • Дополняйте ограничением частоты, а не заменяйте им. CAPTCHA на форме входа и агрессивное ограничение частоты по IP вместе гораздо сильнее, чем каждое по отдельности.
  • Не ставьте CAPTCHA на внутренние админ-страницы. Уберите их за аутентификацию и списки разрешённых IP — защита от ботов нужна для форм, которые обязаны оставаться публичными.

За периметром — сканирование зависимостей с OSV-Scanner

Предыдущие слои либо укрепляют периметр (WAF, защита от ботов), либо чистят репозиторий (секреты). Ни один из них не касается библиотек с известными уязвимостями, уже работающих внутри вашего приложения, — а именно оттуда чаще всего и начинается эксплуатация легаси. Это и лечит сканирование зависимостей.

Почему OSV-Scanner

OSV-Scanner — открытая CLI от Google, построенная поверх базы данных OSV (Open Source Vulnerabilities). OSV — тот источник данных об уязвимостях, к которому под капотом обращаются Dependabot, GitHub Security Advisories, GCP Container Analysis и несколько других сканеров. Это нормализованная машиночитаемая схема, агрегирующая уведомления из npm, PyPI, Go, Cargo, RubyGems, Maven, Packagist, NuGet, пакетов Debian/Alpine/Ubuntu, GitHub Actions и других.

Он читает распространённые lock-файлы (package-lock.json, yarn.lock, Pipfile.lock и т. д.), разрешает каждый пакет в его точно закреплённой версии и сообщает об уязвимостях с идентификаторами CVE, серьёзностью и (там, где OSV это знает) диапазоном версий, который их исправляет.

Запускаем сканирование и читаем вывод

OSV-Scanner можно установить через Go, Homebrew, готовым бинарником или — так же, как gitleaks выше — запустить через Docker. Для единообразия покажем Docker.

Я только что прогнал это по репозиторию, в котором живёт эта статья:

docker run --rm -v "$(pwd):/src" ghcr.io/google/osv-scanner:latest \
    scan source --recursive /src

--recursive проходит по каждому каталогу под /src и находит lock-файлы во вложенных проектах (блог, несколько демо, сервер, черновые эксперименты). Без него сканер осматривает только верхний уровень — нормально для однопроектного репозитория, но у большинства настоящих lock-файлы разбросаны по нескольким каталогам.

Сводка в конце прогона выглядит так:

Total 35 packages affected by 59 known vulnerabilities (2 Critical, 17 High, 37 Medium, 3 Low, 0 Unknown) from 2 ecosystems.
59 vulnerabilities can be fixed.

Полный вывод — таблица с одной строкой на CVE × пакет × источник истины. Несколько представительных строк:

OSV URLCVSSЭкосистемаПакетВерсияИсправленная версияИсточник
GHSA-xq3m-2v4x-88gg9.4npmprotobufjs7.5.47.5.5blog/…/transformers-js-demo/package-lock.json
GHSA-p9ff-h696-f5838.2npmvite7.3.17.3.2blog/package-lock.json
PYSEC-2025-407.5PyPItransformers4.48.34.49.0blog/…/from-scratch/requirements.txt
GHSA-r5fr-rjxr-66jc8.1npmlodash4.17.234.18.0server/package-lock.json

Шесть полей на строку, и все полезны:

  • OSV URL — ведёт прямо к уведомлению на osv.dev (идентификатор CVE, вектор атаки, диапазон затронутых версий, ссылки).
  • CVSS — оценка серьёзности по шкале 0–10. 9+ это Critical, 7–8.9 это High, 4–6.9 это Medium.
  • Ecosystem — из какого реестра пакетов приходит зависимость. Одно и то же имя пакета может существовать в нескольких экосистемах и получать разные CVE.
  • Package и Version — что сейчас закреплено в lock-файле.
  • Fixed version — наименьшая версия, решающая проблему. Часто патч-обновление; иногда минорное или мажорное.
  • Source — из какого lock-файла пришла находка. Критично для монорепозиториев: один и тот же пакет может быть закреплён в разных версиях в разных подпроектах, что мы ровно и видим для vite, protobufjs и picomatch в реальном сканировании.

Обратите внимание, что сводка разбивает счёт двумя способами: 59 известных уязвимостей всего и 59 уязвимостей можно исправить — то есть у каждой есть известная цель обновления. В этом сканировании они совпали, что бывает не всегда.

Колонка Fixed version говорит вам, есть ли путь обновления для каждой строки. Если она заполнена, сканер уже сделал изыскания; остаётся чисто обновление — поднять версию, протестировать, отправить. Если пуста, вы наткнулись на находку без известного исправления, и реакция другая. Следующий раздел разбирает, как разбирать оба вида.

Сканер может также нацелиться на конкретный lock-файл (--lockfile=/path/to/lockfile), на SBOM в формате SPDX или CycloneDX (--sbom=/path/to/sbom.json) или на образ контейнера (osv-scanner scan image my-app:latest) — для сканирования образов нужна локальная установка CLI, а не Docker, поскольку сканеру нужен прямой доступ к файловой системе осматриваемого образа.

Что делать с находками

Прогоните его один раз по легаси-приложению — и почти наверняка получите стену находок: легко десятки, иногда сотни. Для каждой находки что делать решают два вопроса: насколько это серьёзно и есть ли доступное исправление.

Исправимые находки. Основная масса того, что выдаёт сканирование, обычно попадает сюда, потому что активно поддерживаемые пакеты патчатся. Разбирайте по серьёзности:

  1. Critical/High с известным публичным эксплойтом — патчите первым делом, независимо от того, считаете ли вы, что используете этот путь исполнения. Публичной эксплуатации нет дела до вашей мысленной модели кода.
  2. Critical/High без известного эксплойта — патчите в обычном ритме (следующий спринт).
  3. Medium/Low — поднимайте в следующем плановом цикле обновлений. Не давайте им блокировать сборку, иначе зелёного прогона CI на легаси-приложении вы не увидите никогда.

Для всех них работа одной формы: поднять закреплённую версию до Fixed version, прогнать тесты, отправить. Инструменты вроде Dependabot и Renovate (о них ниже) автоматизируют шаг создания PR.

Неисправимые находки. Им нужна другая обработка, и обычно бывает три разновидности:

  1. Патч ещё не выпущен — уязвимость раскрыта, но мейнтейнер не отправил исправление. Отслеживайте, периодически пересканируйте и следите за страницей уведомления OSV на обновления.
  2. Неподдерживаемый пакет — проект мёртв. Либо замените зависимость, либо живите с ней осознанно и добавьте документированное подавление в osv-scanner.toml, чтобы сканирование не помечало её заново при каждом прогоне CI.
  3. Транзитивная зависимость, которую нельзя обновить напрямую — уязвимый пакет затягивается чем-то другим, что не поднялось до более новой родительской версии. Именно этот случай, где вывод OSV-Scanner сочетается с WAF: если пути обновления нет, а у уязвимости есть известный шаблон эксплуатации, напишите правило WAF, блокирующее этот шаблон, как виртуальную заплатку, пока не выйдет исправление сверху.

Разделение важно операционно: исправимые находки — работа для вашего конвейера сборки (PR от Dependabot, автоматические обновления). Неисправимые находки — работа для человека, решающего подавить, заменить или виртуально залатать, — и именно за ними стоит следить, потому что сами по себе они не исчезают.

Альтернативы и когда выбирать какую

  • GCP Artifact Analysis — родной для GCP вариант и, вероятно, сканер с наименьшим трением, который можно добавить, если вы уже на GCP. Artifact Registry автоматически сканирует образы при пуше, используя базу OSV (тот же источник данных, что и у OSV-Scanner выше), а непрерывный анализ продолжает пересканировать сохранённые образы по мере появления новых уязвимостей в OSV — так что вы узнаёте о свежераскрытых CVE в образах, отправленных месяцы назад, без повторного пуша. Находки появляются в интерфейсе Artifact Registry и как уведомления Pub/Sub. Никакого дополнительного инструмента в эксплуатации.
  • Trivy — настоящее надмножество, если вам нужен один инструмент. Сканирует контейнеры, файловые системы, git-репозитории, конфигурации Kubernetes, IaC (Terraform/CloudFormation) и секреты. Берите Trivy, если вам нужно ещё и сканирование контейнеров и IaC и хочется единственный операционный инструмент, работающий где угодно.
  • Dependabot — если вы на GitHub, он уже подключён. Открывает PR на обновление уязвимых зависимостей автоматически. Используйте его рядом с OSV-Scanner: Dependabot для автоматических PR, OSV-Scanner в CI для блокировки сборки по серьёзности.

Разумный минимальный набор: OSV-Scanner в CI (блокирует сборку на новых находках высокой серьёзности) + Dependabot (непрерывно открывает PR на обновление). Если вы уже поставляете контейнеры, либо добавьте Trivy для сканирования образов, либо — если пушите в облачный реестр — опирайтесь на встроенный сканер реестра (Artifact Analysis на GCP, ECR + Inspector на AWS), а не эксплуатируйте ещё один инструмент.

Сканеры — это не гигиена цепочки поставок

Один важный пробел стоит назвать: сканеры ловят известные CVE в закреплённых зависимостях. Они не ловят атаки на цепочку поставок — злонамеренные пакеты, скомпрометированные аккаунты мейнтейнеров, имена-опечатки (lodsh вместо lodash), post-install-скрипты, выкачивающие учётные данные. Это другой класс угроз, требующий других защит: принудительное использование lock-файлов, отключение скриптов жизненного цикла при установке, ревью новых зависимостей перед добавлением, 2FA на аккаунтах реестров пакетов.

NPM Security Cheat Sheet от OWASP — полезный конкретный чек-лист для этого, если ваш стек Node.js: --ignore-scripts, npm audit, скоупнутые пакеты, распознавание опечаток и прочее. Vulnerable Dependency Management Cheat Sheet от OWASP покрывает то же самое без привязки к языку. Оба хорошо сочетаются со слоем на основе сканеров выше: сканеры для известно плохого, гигиена для неизвестно плохого.

Что этот набор покрывает и куда можно пойти дальше

Если вы развернёте три основных слоя плюс сканирование зависимостей, что вы себе на самом деле купили?

Покрыто вполне прилично:

  • Оппортунистические автоматические сканеры и кампании массовой эксплуатации.
  • Известные CVE в зависимостях — при условии, что вы их патчите, когда приходят находки.
  • Утёкшие учётные данные в репозитории (текущие и исторические).
  • SQL-инъекции, XSS, обход путей и инъекция команд во время запроса — блокируются WAF, не доходя до приложения.
  • Очевидный, объёмный слой злоупотреблений ботами на публичных формах.

Всё ещё упущено:

  • Уязвимые шаблоны внутри вашего собственного кода. WAF блокирует полезные нагрузки во время запроса, но не помогает вам найти или исправить лежащий под ними уязвимый код. SAST и DAST ниже — то, с чего вы начнёте это делать.
  • Изъяны логики приложения — всё, что требует знания того, что приложение должно делать. IDOR (запрос к /orders/1234, когда пользователь должен видеть только 1233), сломанная аутентификация или управление сессиями, отсутствующие или несогласованные проверки авторизации, ошибки бизнес-логики вроде пропуска шага оплаты повторной отправкой запроса оформления корзины. Ни одно из этого не выглядит злонамеренным для сканера.
  • Новые уязвимости нулевого дня в приложении или его зависимостях, до появления сигнатур.
  • Упорные противники. Инсайдеры, злоупотребляющие законным доступом. Хорошо обеспеченные операторы ботов, которые платят сервисам решения CAPTCHA и меняют IP быстрее, чем вы успеваете их блокировать.

Что добавить следующим: SAST и DAST

Когда три основных слоя и сканирование зависимостей на месте, естественный следующий шаг — анализировать сам код, а не только трафик, текущий через него. Две дополняющие друг друга техники:

  • SAST (Static Application Security Testing) читает исходники статически и ищет по шаблонам рискованные конструкции: SQL-запросы, собранные склейкой строк, отсутствующие проверки авторизации, команды оболочки, собранные из ввода пользователя, непроверенные перенаправления, прописанные в коде учётные данные. Semgrep — открытый вариант по умолчанию; поддерживаемые сообществом наборы правил вроде p/security-audit и p/owasp-top-ten дают покрытие по большинству языков без возни с настройкой CodeQL или SonarQube. Запускайте в CI и блокируйте сборку на находках высокой серьёзности.
  • DAST (Dynamic Application Security Testing) делает обратное — запускает приложение и зондирует его снаружи, обходя маршруты и пробуя настоящие атакующие полезные нагрузки. Мощно для приложения-чёрного-ящика именно потому, что не требует исходного кода; сканер — это синтетический атакующий. OWASP ZAP — открытый вариант по умолчанию, с пассивным базовым сканированием, безопасным для стейджа или продакшена, и разрушительным полным активным сканированием, которое стоит запускать только по стейджу с одноразовыми данными. Burp Suite — коммерческий отраслевой стандарт; Nuclei дополняет любой из них быстрым покрытием шаблонов CVE.

Разумное расширение набора: Semgrep на каждом PR, базовое сканирование ZAP на каждом деплое в стейдж, полное аутентифицированное сканирование ZAP по ночному или недельному расписанию. Аутентифицированное сканирование для DAST на легаси-приложении важно непропорционально сильно — неаутентифицированное сканирование видит только страницу входа, а все интересные эндпоинты за ней.