Как No-Op валидатор Grafana превращает анонимный доступ в Pre-Auth SSRF
TL;DR
- Grafana OSS поставляется с no-op валидатором запросов для эндпоинта прокси источников данных. Он всегда возвращает
nil. Никакой защиты от SSRF. - В сочетании с двумя конфигурациями по умолчанию это позволяет неаутентифицированным пользователям проксировать HTTP-запросы к любому внутреннему сервису, доступному с сервера Grafana.
- Сканирование через Shodan 1 000 случайных инстансов обнаружило ~7 800 открытых в интернет инстансов Grafana с включённым анонимным доступом. Эксплуатируется напрямую, учётные данные не требуются.
- На EC2 с включённым IMDSv1 это означает полную кражу AWS-учётных данных без логина: AccessKeyId, SecretAccessKey, токен сессии.
- Grafana Enterprise поставляется с настоящим валидатором. OSS — нет. Это намеренное разделение продуктов.
- Отправлено в программу bug bounty Grafana, отмечено как вне области применения. Отслеживается как CVE-2026-39104, присвоено MITRE.
Предыстория
Прокси источников данных Grafana — это легитимная функция. Вы настраиваете источник данных (Prometheus, InfluxDB и т.д.) с URL бэкенда, и Grafana проксирует к нему запросы от имени пользователей дашборда. Это позволяет держать учётные данные на стороне сервера и избегать проблем с CORS.
Эндпоинт выглядит следующим образом:
| |
При нормальном использовании: дашборд выполняет структурированный запрос, Grafana пересылает его источнику данных, ответ возвращается обратно. Чисто и понятно.
Проблема: этот эндпоинт также действует как сырой HTTP-прокси. Он пересылает любой путь, который вы добавляете, напрямую на настроенный URL источника данных. В OSS-сборке ничего не проверяет, куда этот URL ведёт.
Цепочка уязвимостей
Три компонента объединяются, чтобы создать pre-auth SSRF:
1. No-Op валидатор (pkg/services/validations/oss.go:11)
| |
Это вся реализация. Всегда nil. Никакой проверки IP, никаких ограничений схемы, никакого разрешения имён хостов. Это подключено как production-валидатор для всех OSS-сборок через wireexts_oss.go.
Для сравнения: Grafana Enterprise поставляется с EnterpriseDataSourceRequestValidator, который выполняет реальную проверку диапазонов IP. Grafana осведомлена о риске SSRF. Они решили не распространять защиту на пользователей OSS.
2. Пустой вайтлист пропускается (pkg/api/pluginproxy/ds_proxy.go:402)
| |
conf/defaults.ini:
| |
Это задокументировано. Но по умолчанию никогда не используется.
3. Анонимные пользователи по умолчанию получают datasources:query
При создании источника данных Grafana автоматически предоставляет роли Viewer права datasources:query (pkg/services/datasources/service/datasource.go:385):
| |
Когда auth.anonymous.enabled = true, анонимные пользователи наследуют роль Viewer. Эндпоинт прокси требует только datasources:query. Логин не нужен.
Итого: анонимный доступ включён → анонимный пользователь получает Viewer → Viewer может вызвать эндпоинт прокси → прокси не имеет валидатора → запрос идёт туда, куда указывает URL источника данных.
Proof of Concept
Лабораторная установка: Grafana OSS с GF_AUTH_ANONYMOUS_ENABLED=true, источник данных InfluxDB, указывающий на внутренний mock-сервис на порту 8888 (не открытый для хоста, доступный только внутри Docker-сети).
Шаг 1: Подтверждение анонимного доступа (без учётных данных)
| |
Шаг 2: Получение UID источника данных
Эндпоинт прокси требует UID источника данных в пути:
/api/datasources/proxy/uid/{UID}/...
Без UID нет цели. /api/datasources возвращает полный список, включая UID, URL бэкенда и типы источников данных. В старых версиях Grafana этот эндпоинт доступен роли Viewer, то есть анонимные пользователи могут его читать без учётных данных:
| |
Более новые версии (10+) ограничивают этот эндпоинт для Editor и выше, возвращая 403 для Viewer. Это добавляет сложности, но не блокирует атаку. Альтернативные источники UID:
- JSON дашборда: Любой дашборд, использующий источник данных, встраивает UID в определения своих панелей.
GET /api/dashboards/uid/{dashboard-uid}возвращает полный JSON, доступный для Viewer. Ищите.panels[].targets[].datasource.uid. - Публичные дашборды: Инстансы с публичными дашбордами раскрывают UID источников данных в исходном коде отображаемой страницы.
- Перебор: UID в Grafana следуют предсказуемому буквенно-цифровому формату. Низкочастотный перебор через эндпоинт прокси осуществим. Ответ 200 или 502 подтверждает валидный UID; 404 — нет.
В лаборатории /api/datasources возвращает UID напрямую, так как инстанс работает на более старой конфигурации.
Шаг 3: Инициирование SSRF (без заголовка Authorization)
Шаг 2 раскрыл только метаданные: конфигурацию источника данных, хранящуюся в базе данных Grafana. Наличие "url":"http://internal-mock:8888" в списке источников данных не означает, что внутренний сервис достижим. С машины атакующего — нет.
На этом шаге происходит реальный SSRF. Эндпоинт прокси инструктирует сервер Grafana сделать исходящий HTTP-запрос к настроенному URL источника данных и вернуть ответ. Атакующий никогда не контактирует с внутренним сервисом напрямую. Это делает Grafana, из своей сетевой позиции, и передаёт результат обратно.
Атакующий (интернет) ──► Эндпоинт прокси Grafana
│
│ запрос на стороне сервера
▼
internal-mock:8888 (не открыт в интернет)
│
│ ответ
▼
Grafana ──► Атакующий
| |
У внутреннего сервиса нет открытых портов на хосте. Прямой запрос с машины атакующего завершится таймаутом. Этот ответ вернулся, потому что сервер Grafana сделал запрос от имени атакующего.
Полный PoC (лабораторная установка на Docker, автоматическое перечисление источников данных, обработка ошибок) на GitHub:
github.com/awallplace/grafana-datasource-ssrf
Реальный Impact
Цели, достижимые из сетевой позиции сервера Grafana:
AWS IMDSv1:
Предусловия: Grafana работает на EC2/ECS/EKS с включённым IMDSv1, URL источника данных указан как http://169.254.169.254/. Учётные данные со стороны атакующего не требуются.
| |
| |
Полные IAM-учётные данные возвращены неаутентифицированному вызывающему. GCP (metadata.google.internal) и Azure (169.254.169.254) раскрывают тот же паттерн.
Kubernetes (развёртывания внутри кластера):
| |
Перечисление внутренней сети (аутентифицированный, Editor+): Editor может указать источник данных на любой внутренний IP:порт, а затем зондировать через прокси. Коды ответов напрямую отражают состояние хоста:
| Ответ | Значение |
|---|---|
200 + данные сервиса | Хост активен, порт открыт, сервис работает |
502 Bad Gateway | Хост активен, порт закрыт или неверный протокол |
| Таймаут | Хост недоступен или за файрволом |
Перебор по IP и портам позволяет картографировать внутреннюю сеть Grafana снаружи. Сервер Grafana осуществляет зондирование; атакующий видит только коды ответов. Прямой доступ к внутренней сети не требуется.
Случай: Раскрытие источника данных в ходе исследования
Во время сканирования через Shodan один инстанс имел включённый анонимный доступ и возвращал список источников данных на неаутентифицированные запросы. Были настроены два источника данных, один из которых указывал на внутренний API мониторинга:
| |
Проксирование через источник данных InfluxDB без учётных данных:
| |
Неаутентифицированный запрос вернул актуальную версию InfluxDB и имена баз данных сервиса без открытых портов в интернет. На этом исследование было остановлено. Цель состояла в подтверждении достижимости, а не в извлечении данных.
Масштаб: данные Shodan
Пассивное сканирование 1 000 случайно выбранных инстансов Grafana из 206 310 проиндексированных Shodan развёртываний, проверка только /api/org (публичный эндпоинт, без авторизации, возвращает название организации при анонимном доступе):
| Метрика | Значение |
|---|---|
| Размер выборки | 1 000 |
| Достижимые инстансы | 987 (98,7%) |
| Анонимный доступ включён | 38 (3,9% от достижимых) |
| |
Две отдельные поверхности атаки:
Поверхность 1: Pre-auth (~7 800 инстансов): Анонимный доступ включён. Учётные данные не требуются. Неаутентифицированный атакующий может немедленно перечислить источники данных и проксировать запросы к любому настроенному URL бэкенда. Именно этот сценарий измерялся в ходе сканирования Shodan.
Поверхность 2: Post-auth (~206 000 инстансов): No-op валидатор присутствует в каждой сборке Grafana OSS независимо от конфигурации аутентификации. Атакующий со скомпрометированной учётной записью Editor (через фишинг, подбор учётных данных, утечку API-ключа или взлом SSO) может:
- Создать новый источник данных с URL, указывающим на любую внутреннюю цель (
http://169.254.169.254/,https://kubernetes.default.svc, внутреннюю базу данных и т.д.) - Использовать эндпоинт прокси источника данных для пересылки запросов к этой цели
- Получить полный ответ: IAM-учётные данные, данные сервиса или ответы внутреннего API
Валидатор никогда не запускается. Вайтлист по умолчанию пуст. Единственное отличие от поверхности 1 в том, что эксплойту предшествует шаг аутентификации.
Это важное различие: поверхность 2 требует скомпрометированной учётной записи — отдельного предусловия. Но это означает, что ~206 000 инстансов, которые выглядят «безопасными» из-за отключённого анонимного доступа, находятся в одном взломе учётных данных от того же внутреннего раскрытия.
Диапазон версий в выборке с включённым анонимным доступом: от 6.6.1 до 12.4.1. Два инстанса с патч-релизами +security-01. Патчи безопасности Grafana не устранили эту проблему. Она присутствует как минимум в шести основных линейках релизов.
Серьёзность
| |
| Метрика | Значение | Обоснование |
|---|---|---|
| Attack Vector | Network | Эксплуатируется удалённо через интернет |
| Attack Complexity | Low | Не требует гонок состояний или специальной подготовки |
| Privileges Required | None | Анонимные пользователи автоматически получают роль Viewer; datasources:query предоставляется Viewer при создании источника данных. Логин не нужен |
| User Interaction | None | Действий жертвы не требуется |
| Scope | Changed | Воздействие выходит за пределы Grafana на внутренние сервисы, доступные серверу, но недоступные атакующему |
| Confidentiality | High | Полные тела ответов от внутренних сервисов, включая облачные учётные данные |
| Integrity | None | Прокси только для чтения; записи через этот механизм не происходит |
| Availability | None | Нарушений работы сервиса нет |
При отключённом анонимном доступе уязвимость всё равно существует для пользователей уровня Editor (требуется создание источника данных). Приведённая оценка отражает условие pre-auth, которое является реалистичным наихудшим случаем с учётом ~7 800 инстансов с анонимным доступом, обнаруженных в Shodan.
«Это фича, а не баг»
Контраргумент: администратор включил анонимный доступ, администратор указал источник данных на внутренний URL — следовательно, это неправильная конфигурация оператора.
Проблема с таким подходом: включение анонимного доступа — это намеренное решение администратора, но предоставление этим анонимным пользователям сырого HTTP-прокси-доступа ко всем настроенным URL источников данных — нет. Разрешение datasources:query охватывает как структурированные запросы дашборда, так и эндпоинт сырого прокси. Две совершенно разные вещи под одним именем разрешения.
Более красноречивым свидетельством является EnterpriseDataSourceRequestValidator. Он существует. Он выполняет IP-валидацию. Он подключён в Enterprise-сборки и заменён no-op заглушкой в OSS. Это не было упущением: он был реализован, а затем намеренно не включён. Grafana точно знает, куда прокси может достучаться без него.
Grafana подтвердила это через ответ своей программы bug bounty:
«Когда вы включаете анонимный режим, для приложения это намеренное поведение — по умолчанию считать вас viewer. Grafana Enterprise обрабатывает этот случай для клиентов, которым нужно иное поведение. OSS в результате ведёт себя так, как должен.»
Аргумент последователен как продуктовое решение. Он не последователен как позиция по безопасности. «Администратор включил анонимный доступ» и «администратор намеревался дать анонимным пользователям сырой HTTP-прокси-доступ ко всем настроенным URL источников данных» — это два разных утверждения. Истинно только одно из них.
Устранение
Замените no-op валидатор OSS на что-то, что реально работает:
| |
Пока вы ожидаете исправления, несколько мер помогут в промежутке:
- Установите
data_source_proxy_whitelist, чтобы ограничить, какие эндпоинты источников данных достижимы - Обеспечьте соблюдение IMDSv2 в облачных развёртываниях (
HttpTokens: requiredна EC2), что ограничивает раскрытие облачных метаданных при SSRF - Относитесь к
datasources:queryкак к доступу к прокси, а не только к запросам дашборда, потому что прямо сейчас это и то, и другое
Если вы запускаете Grafana с включённым анонимным доступом, проверьте свои URL источников данных сегодня.
Хронология раскрытия
| Дата | Событие |
|---|---|
| 2026-03-31 | Уязвимость выявлена в ходе статического анализа исходного кода |
| 2026-03-31 | PoC-лаборатория разработана и подтверждена |
| 2026-03-31 | Проведено сканирование раскрытия через Shodan |
| 2026-03-31 | Отправлено в программу bug bounty Grafana Labs (Intigriti) |
| 2026-04-01 | Отмечено как вне области применения: «Any reports of SSRF against the data source proxy endpoint» |
| 2026-04-01 | CVE запрошен у MITRE |
| 2026-04-08 | Публичное раскрытие |
| 2026-05-01 | CVE-2026-39104 присвоен MITRE (затрагивает Grafana OSS v6.6.1 — 12.4.1) |
Grafana отметила это как вне области применения через свою программу bug bounty. MITRE присвоила идентификатор CVE-2026-39104, охватывающий Grafana OSS v6.6.1 — 12.4.1. Цель этого материала — сделать риск видимым для операторов, управляющих ~7 800 затронутыми инстансами.