Почему ваша LLM-платформа уязвима: аудит безопасности AI-сервиса изнутри
Почему ваша LLM-платформа — следующая цель: аудит безопасности AI-сервиса изнутри
Disclaimer: Всё описанное — результатсанкционированного аудита безопасностипо договору. Уязвимости ответственно раскрыты, ключи ротированы, домены и IP изменены. Статья — для понимания,не для воспроизведения.
Мы искали уязвимости в RAG-платформе с десятками тысяч пользователей — а нашли доступ ко всей инфраструктуре и API-ключам с бюджетом в сотни тысяч долларов. Две недели мы строили сложные цепочки: SSRF через LangChain, инъекции в промпты, HTTP smuggling, CVE в десериализации. Ни одна не дала результата. А потом мы сделали одинcurlк открытому порту — и получили все ключи за 5 минут.
Эта статья —не гайд по взлому. Это разбор того, почему LLM-инфраструктура создаёт принципиально новые риски, какие ошибки мы раз за разом видим в AI-стартапах, и на что стоит обратить внимание, если вы строите что-то похожее.
Почему LLM-платформы — особый класс целей
Прежде чем переходить к конкретике — важно понять, чем аудит AI-платформы отличается от обычного SaaS.
Обычное веб-приложение:
LLM-платформа:
Разница принципиальная:
- Дорогие секреты в обороте.API-ключи от Anthropic/OpenAI — это не пароли от тестовой БД. Это прямой доступ к биллингу на десятки тысяч долларов в месяц.
- User-controlled routing.В классическом SaaS пользователь отправляет данные. В LLM-платформе пользователь может косвенно влиять на то,кудасервер отправит HTTP-запрос — через выбор модели, хоста, параметров.
- Прокси-архитектура с передачей секретов.Ключ извлекается из базы, передаётся между сервисами, оседает в памяти процесса. Каждый этап — потенциальная точка утечки.
- Быстрый MVP → безопасность потом.AI-стартапы торопятся. Docker-compose в продакшене, дефолтные секреты, отсутствие сегментации — не исключение, а правило.
Держа это в голове, посмотрим, как это выглядит на практике.
Визуально: где ломается LLM-платформа
Каждая стрелка — потенциальный вектор. Но критичнее всего оказался самый простой: открытый Docker, в обход всей цепочки.
Содержание
- Инфраструктура и разведка
- Аутентификация: JWT с дефолтным секретом
- SSRF через LLM-провайдер: новый класс уязвимости
- Охота за API-ключами: что мы пробовали и почему не получилось
- Admin-панель: что расскажут JS-бандлы
- Открытый Docker API: как одна ошибка обесценивает всё остальное
- Итоги, уроки и рекомендации
1. Инфраструктура и разведка
Что мы аудировали
RAG-as-a-Service на стекеLangChain + Flask + Next.js + Docker Swarm. Пользователи загружают документы, выбирают модель (Claude, GPT, Grok, DeepSeek, Gemini — всего 10+), получают ответы через единый API.
Карта инфраструктуры
API Backend
Flask + Nginx
Аутентификация, бизнес-логика
Admin Panel
Next.js App Router
Управление кластером (IP-restricted)
Flask + Celery + Redis
Обработка запросов, маршрутизация к LLM
Непосредственный вызов LLM-провайдеров
PostHog (self-hosted, Hobby tier)
Первое наблюдение:admin-панель, RAG-прокси и аналитика живут на одном сервере. Один IP, один Nginx, общие сетевые правила. Компрометация одного сервиса расширяет поверхность атаки на все остальные.
Что выявило сканирование
Помимо ожидаемых портов (80/443, прокси на 5556), сканирование обнаружило порт2375— Docker Remote API. По умолчанию он работает без аутентификации. Мы зафиксировали это и продолжили систематический аудит — к Docker вернёмся в главе 6.
DNS-записи добавили штрих:DMARC p=none— нулевая защита от email-спуфинга. Для платформы с тысячами пользователей это прямой путь к фишингу от имени admin@platform.
2. Аутентификация: JWT с дефолтным секретом
В рамках white-box аудита (с доступом к исходникам — стандартная практика) мы обнаружили типичный антипаттерн:
Разработчик оставил «напоминание себе» в дефолтном значении — и оно уехало в продакшен. Переменная окружения не была установлена.
Даже без исходников такие секреты подбираются за минуты:hashcat -m 16500перебирает стандартные словари, куда входят строки именно такого вида.
Последствия
С известным JWT-секретом мы подделали admin-токен и через легитимные API-вызовы извлекли:
- API-токендля RAG-прокси (используется в цепочке получения LLM-ключей)
- Полный кластер: 10 LLM-нод, 3 SD-ноды на RunPods GPU, внутренние IP-адреса, SSH-порты
- Профили пользователей: балансы, email’ы, ключи интеграций (Tavily API)
- Конфигурациивсех нод: хосты, порты, параметры моделей
Всё через штатные API-эндпоинты. Ни одного SQL injection — зачем, если JWT forgery делает их ненужными?
Урок:os.getenv("KEY", "default-value")— антипаттерн для секретов. ЕслиKEYне установлен, приложение должнопадать при старте, а не работать с дефолтом. CI/CD должен проверять наличие обязательных переменных окружения до деплоя.
3. SSRF через LLM-провайдер: новый класс уязвимости
Самая интересная часть аудита с точки зрения AI-специфики.
Суть проблемы
Платформа поддерживает self-hosted модели через Ollama. При обработке запроса worker делает HTTP-вызов к хосту, которыйприходит из пользовательских данных:
Хост не валидируется. Нет whitelist’а, нет проверки на приватные IP. Подставляяollama.attacker-ip.nip.io(nip.io — wildcard DNS, резолвящий*.1.2.3.4.nip.ioв1.2.3.4), мы заставили worker отправить HTTP-запрос на контролируемый нами сервер.
Что это даёт
На внешнем сервере — HTTP-обработчик, логирующий входящие запросы. Результат:worker отправляет POST с промптом пользователя на произвольный хост. Классический blind SSRF, но с LLM-спецификой.
Более того: если сервер возвращает валидный JSON в формате Ollama, платформа принимает его как настоящий ответ модели. Мы можем вернутьлюбой текстот имени «Claude» или «GPT» — этоcanary injection, подмена ответов на уровне инфраструктуры.
Для RAG-платформы, где пользователи доверяют ответам ИИ и загружают конфиденциальные документы, это серьёзный вектор: от фишинга до кражи данных через подмену контекста.
Границы SSRF
Доступность
Внешние хосты
Нет egress-фильтрации
Другие ноды кластера
Внутренняя сеть (10.0.0.x)
Cloud metadata
Не cloud-инфраструктура
SSRF ограничен форматом Ollama (POST/api/chatс JSON), что лимитирует pivoting. Но для эксфильтрации промптов и подмены ответов — достаточно.
Урок:Любой параметр, превращающийся в URL для HTTP-запроса — потенциальный SSRF. В LLM-платформах таких параметров больше обычного: хосты моделей, эндпоинты embeddings, URL источников для RAG. Валидируйте хосты через whitelist, проверяйте DNS resolution на приватные диапазоны.
4. Охота за API-ключами: что мы пробовали и почему не получилось
Четыре дня, 15+ техник, ноль перехваченных ключей. Разбор «почему не получилось» не менее поучителен, чем успешные находки.
Как устроена передача ключей
Ключ путешествует: БД → API → Proxy → Worker → провайдер. Каждый этап — потенциальная точка перехвата. Но на практике каждый оказался защищён — где-то осознанно, где-то случайно.
Три категории протестированных атак
Template-инъекции
LangChainPromptTemplateиспользуетstr.format(). Мы проверили, доступны ли секреты:
- {api_key},{ANTHROPIC_API_KEY}→KeyError(нет в контексте)
- {{ config }}→ литеральная строка (это не Jinja2)
- Attribute traversal через format → ограничен Python’ом
- LangChain deserialization CVE ({"lc":1, "type":"secret"}) → данные не проходят черезloads()
Вывод:PromptTemplateпо умолчанию безопасен. Но если бы разработчик включилtemplate_format="jinja2"— SSTI был бы реален.
Сетевые атаки
Почему не сработало
Перенаправление base_url провайдера
URL захардкожен в helper’е
VLLMOpenAI endpoint hijack
Ошибка evaluate до создания клиента
HTTP request smuggling (CL.TE)
Nginx и Gunicorn парсят одинаково
CRLF injection в имени хоста
httpx строго валидирует URL
Redis injection через SSRF
Redis в Docker-сети, не на localhost
IP-spoofing (X-Forwarded-For)
Nginx перезаписывает заголовок
Вывод:Современные HTTP-библиотеки и правильно настроенный reverse proxy эффективно блокируют инъекции на транспортном уровне.
Логические атаки
Почему не сработало
Race condition (50 параллельных)
Ошибка детерминированная
Перебор 2000 node_id
Forbidden или та же ошибка
Error oracle (traceback)
Flask production — трейсбеки скрыты
DNS rebinding
Один DNS-запрос, нет re-resolve
Вывод:Production-конфигурация Flask без debugger’а — критически важна. СFLASK_DEBUG=1error oracle мог бы сработать.
Почему 15 техник провалились
Ключевая причина оказалась неожиданной:функция evaluate содержала баг— возвращала 1 элемент вместо 2. Ошибкаnot enough values to unpackпроисходиладосоздания LLM-клиента. Ключ извлекался из базы, но не доходил до стадии, где его можно перехватить.
Ирония ситуации: баг в коде, который мы пытались эксплуатировать,защищал ключи лучше любого Vault’а.
5. Admin-панель: что расскажут JS-бандлы
Публичная карта приватного API
Admin-панель на Next.js отдаёт минифицированные JS-бандлы. Минификация — не защита. Анализ раскрыл:
Модель аутентификации— cookieauth_token, установка черезPOST /v1/admin/loginс username/password. Не Bearer, как в основном API — отдельная сессия.
Полная карта маршрутов— 18 admin-страниц: управление нодами кластера, GPU-пулом RunPods, кредитами, подписками, email-шаблонами, промокодами, релизами.
CORS-конфигурация:
API доверяетlocalhost:3000— внутреннему Next.js dev-серверу. Если получить SSRF с этого хоста — можно обойти IP-whitelist.
Раскрытие внутреннего URL бэкенда:
Почему это проблема
JS-бандлы доступныбез аутентификации. Атакующий получает полную структуру admin API: все маршруты, параметры, ролевую модель, имена cookie — не отправив ни одного запроса к защищённым эндпоинтам. Это значительно ускоряет планирование атаки.
Урок:Разделяйте admin-бандлы. Не включайте маршруты dashboard в публичный JS. Используйте server components Next.js для чувствительной логики. И помните: минификация ≠ безопасность.
6. Открытый Docker API: как одна ошибка обесценивает всё остальное
После двух недель сложных техник — SSRF через wildcard DNS, CVE в LangChain, HTTP smuggling — мы вернулись к порту, обнаруженному в первый день.
Docker Remote API. Один HTTP-запрос:
Без аутентификации.Без TLS. Без какой-либо защиты.
Что это означает на практике
Через Docker API доступно чтение метаданных контейнеров, включаяпеременные окружения:
Все секреты платформы. В одном запросе.
Помимо чтения, Docker API позволяет создавать контейнеры с монтированием хостовой файловой системы, деплоить Swarm-сервисы, выполнять команды внутри работающих контейнеров. По сути этонеаутентифицированный root-доступк серверу.
Это классическая, хорошо задокументированная ошибка — Docker daemon по умолчанию слушает на TCP без TLS.Docker documentationпрямо предупреждает об этом. И тем не менее мы встречаем её снова и снова.
Особенно иронично то, что admin API был закрыт IP-whitelist’ом в Nginx (и мы не смогли его обойти за две недели), evaluate защищён от утечек благодаря случайному багу, SSRF ограничен форматом Ollama — а Docker API стоял открытым и обесценивалвсеэти меры разом.
Главный парадокс аудита
Мы потратили4 дняна SSRF-цепочки, CVE в LangChain, HTTP smuggling и race conditions — и получилиноль ключей.А затем сделалиодин HTTP-запроск Docker API — и получиливсе ключи разом.Две недели сложных техник. Пять минут простого скана портов. Результат — один и тот же.
Урок:Безопасность определяется самым слабым звеном. Можно выстроить сложную защиту API-ключей на уровне приложения — и потерять всё из-за открытого порта оркестратора. Регулярный аудит портов и сетевых политик — не менее важен, чем код.
7. Итоги, уроки и рекомендации
Полная картина
За 14 дней аудита мы обнаружили27 уязвимостей. Вот как они складываются в цепочку:
Уязвимость
AI-специфично?
Docker API без auth
Нет — классическая инфра-ошибка
JWT дефолтный секрет
Нет — классическая ошибка
Ollama SSRF (nip.io)
Да— user-controlled model host
Canary injection
Да— подмена ответов LLM
Ключи plain text в БД
Частично — дорогие LLM-ключи
DMARC p=none
PostHog Hobby в prod
CORS localhost + info disclosure
Характерно:самые критичные уязвимости — не AI-специфичные. Это базовые ошибки инфраструктуры. А AI-специфичные находки (SSRF через model host, canary injection) — серьёзны, но secondary.
Что это говорит об отрасли
AI не добавляет безопасность — он добавляетповерхность атаки. LLM — это просто ещё один HTTP-клиент с дорогими ключами. И если базовая инфраструктура не защищена, никакие AI-специфичные меры не помогут.
Паттерн, который мы видим в AI-стартапах снова и снова:
- Быстрый MVP на docker-compose— переезжает в прод без ревизии
- Дефолтные секреты— «поменяем позже» (не поменяют)
- Плоская сеть— все сервисы видят друг друга, egress не ограничен
- Ключи как строки— plain text в ENV, в БД, в HTTP между сервисами
- User-controlled routing— хосты моделей, URL источников для RAG
Рекомендации
Немедленные действия
Открытый Docker API
Закрыть порт, TLS mutual auth, Docker contexts
Дефолтный JWT-секрет
Генерировать ≥256 бит, fail-fast при отсутствии ENV
Ollama SSRF
Whitelist хостов, DNS-валидация на приватные диапазоны
Ключи plain text
HashiCorp Vault / AWS Secrets Manager
DMARC p=none
p=reject+ строгий SPF/DKIM
Архитектурные
- Сегментация: admin, proxy, worker — разные серверы и VPC
- Egress firewall: worker ходит только на whitelisted LLM-провайдеров
- Proxy pattern для ключей: ключ никогда не покидает secure enclave; прокси сам делает вызов к провайдеру
- Secret rotation: автоматическая ротация через Vault с TTL
- Мониторинг: алерты на аномальный DNS, новые Docker-сервисы, исходящие соединения worker’ов
- CI/CD:gitleaks/trufflehogв пайплайне, проверка обязательных ENV перед деплоем
Чеклист для LLM-платформ
Если вы строите что-то похожее — пройдитесь по списку:
- [ ] JWT-секрет сгенерирован криптографически, не дефолтный
- [ ] Docker API закрыт или за TLS mutual auth
- [ ] Хосты моделей валидируются через whitelist
- [ ] API-ключи в secret manager, не в ENV и не в БД plain text
- [ ] Egress-трафик worker’ов ограничен
- [ ] Admin-панель на отдельном хосте с отдельной аутентификацией
- [ ] DMARC p=reject, SPF/DKIM настроены
- [ ] Flask/Django не в debug-режиме в production
- [ ] JS-бандлы не содержат admin-маршруты
- [ ] Регулярный аудит открытых портов
Таймлайн аудита
Ключевые находки
Разведка, code review
Открытый Docker 2375, JWT дефолтный секрет, SSRF в Ollama
Аутентификация
JWT forgery → полный дамп кластера, профилей, кластерных данных
Ollama SSRF через nip.io, canary injection, зондирование сети
Попытки перехвата ключей
15+ техник (template injection, smuggling, CVE, race) — все неуспешны
День 11-12
Admin-панель
Реверс JS-бандлов, cookie auth, CORS, полная карта admin API
Docker API
Чтение ENV — все API-ключи извлечены
Responsible disclosure, ротация ключей
Если прямо сейчас у вас:
- docker-composeв проде без ревизии сетевых политик
- API-ключи в переменных окружения или plain text в базе
- Нет egress-фильтрации на worker’ах
- JWT-секрет, который «поменяем потом»
— ваша LLM-платформауже потенциально уязвима, даже без сложных атак. Не нужны ни SSRF, ни CVE. Достаточно одного открытого порта.
Все уязвимости закрыты. Ключи ротированы. Если строите LLM-платформу — используйте чеклист выше.