Почему ваша LLM-платформа — следующая цель: аудит безопасности AI-сервиса изнутри

Страницы:  1

Ответить
 

Professor Seleznov


Disclaimer: Всё описанное — результат санкционированного аудита безопасности по договору. Уязвимости ответственно раскрыты, ключи ротированы, домены и IP изменены. Статья — для понимания, не для воспроизведения.
Мы искали уязвимости в RAG-платформе с десятками тысяч пользователей — а нашли доступ ко всей инфраструктуре и API-ключам с бюджетом в сотни тысяч долларов. Две недели мы строили сложные цепочки: SSRF через LangChain, инъекции в промпты, HTTP smuggling, CVE в десериализации. Ни одна не дала результата. А потом мы сделали один curl к открытому порту — и получили все ключи за 5 минут.
Эта статья — не гайд по взлому. Это разбор того, почему LLM-инфраструктура создаёт принципиально новые риски, какие ошибки мы раз за разом видим в AI-стартапах, и на что стоит обратить внимание, если вы строите что-то похожее.
-
Почему LLM-платформы — особый класс целей
Прежде чем переходить к конкретике — важно понять, чем аудит AI-платформы отличается от обычного SaaS.
Обычное веб-приложение:
Пользователь → API → База данных
LLM-платформа:
Пользователь → API → Прокси → Evaluate (ключи) → Worker → LLM-провайдер
↕ ↕
PostgreSQL api.anthropic.com
(пул ключей) (x-api-key: sk-ant-...)
Разница принципиальная:
  • Дорогие секреты в обороте. API-ключи от Anthropic/OpenAI — это не пароли от тестовой БД. Это прямой доступ к биллингу на десятки тысяч долларов в месяц.
  • User-controlled routing. В классическом SaaS пользователь отправляет данные. В LLM-платформе пользователь может косвенно влиять на то, куда сервер отправит HTTP-запрос — через выбор модели, хоста, параметров.
  • Прокси-архитектура с передачей секретов. Ключ извлекается из базы, передаётся между сервисами, оседает в памяти процесса. Каждый этап — потенциальная точка утечки.
  • Быстрый MVP → безопасность потом. AI-стартапы торопятся. Docker-compose в продакшене, дефолтные секреты, отсутствие сегментации — не исключение, а правило.
Держа это в голове, посмотрим, как это выглядит на практике.
Визуально: где ломается LLM-платформа
╔══════════════════════════╗
Пользователь ║ Точки компрометации: ║
│ ╚══════════════════════════╝

┌─────────┐
│ API │◄──── JWT forgery (дефолтный секрет)
└────┬────┘

┌─────────┐
│ RAG │◄──── SSRF (user-controlled model host)
│ Proxy │
└────┬────┘

┌─────────┐
│Evaluate │ Ключ извлекается из БД (plain text)
└────┬────┘ и передаётся дальше по цепочке

┌─────────┐
│ Worker │──── canary injection (подмена ответов)
└────┬────┘

api.anthropic.com
x-api-key: sk-ant-... Docker API (порт 2375)

Все ENV контейнеров
= все ключи сразу ◄────── один curl
Каждая стрелка — потенциальный вектор. Но критичнее всего оказался самый простой: открытый 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)
RAG Proxy Flask + Celery + Redis Обработка запросов, маршрутизация к LLM
Worker Python Непосредственный вызов LLM-провайдеров
Analytics 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 аудита (с доступом к исходникам — стандартная практика) мы обнаружили типичный антипаттерн:
JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key-change-in-production")
Разработчик оставил «напоминание себе» в дефолтном значении — и оно уехало в продакшен. Переменная окружения не была установлена.
Даже без исходников такие секреты подбираются за минуты: 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-вызов к хосту, который приходит из пользовательских данных:
# Упрощённая логика маршрутизации
if host.startswith("ollama."):
# Worker делает POST http://{host}:{port}/api/chat
return OllamaHelper(host=host, port=port)
Хост не валидируется. Нет 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) Нет Firewall
Cloud metadata Нет Не cloud-инфраструктура

SSRF ограничен форматом Ollama (POST /api/chat с JSON), что лимитирует pivoting. Но для эксфильтрации промптов и подмены ответов — достаточно.
Урок: Любой параметр, превращающийся в URL для HTTP-запроса — потенциальный SSRF. В LLM-платформах таких параметров больше обычного: хосты моделей, эндпоинты embeddings, URL источников для RAG. Валидируйте хосты через whitelist, проверяйте DNS resolution на приватные диапазоны.

-
4. Охота за API-ключами: что мы пробовали и почему не получилось
Четыре дня, 15+ техник, ноль перехваченных ключей. Разбор «почему не получилось» не менее поучителен, чем успешные находки.
Как устроена передача ключей
┌─────────────┐         ┌───────────┐         ┌──────────┐
│ PostgreSQL │ HTTP │ RAG │ invoke │ Worker │
│ aikeys_pool │────────→│ Proxy │────────→│ │
│ (plain text)│ [key] │ │ │ │──→ api.anthropic.com
└─────────────┘ └───────────┘ └──────────┘ x-api-key: sk-ant-...
Ключ путешествует: БД → API → Proxy → Worker → провайдер. Каждый этап — потенциальная точка перехвата. Но на практике каждый оказался защищён — где-то осознанно, где-то случайно.
Три категории протестированных атак
Template-инъекции
LangChain PromptTemplate использует 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=1 error oracle мог бы сработать.
Почему 15 техник провалились
Ключевая причина оказалась неожиданной: функция evaluate содержала баг — возвращала 1 элемент вместо 2. Ошибка not enough values to unpack происходила до создания LLM-клиента. Ключ извлекался из базы, но не доходил до стадии, где его можно перехватить.
Ирония ситуации: баг в коде, который мы пытались эксплуатировать, защищал ключи лучше любого Vault’а.
-
5. Admin-панель: что расскажут JS-бандлы
Публичная карта приватного API
Admin-панель на Next.js отдаёт минифицированные JS-бандлы. Минификация — не защита. Анализ раскрыл:
Модель аутентификации — cookie auth_token, установка через POST /v1/admin/login с username/password. Не Bearer, как в основном API — отдельная сессия.
Полная карта маршрутов — 18 admin-страниц: управление нодами кластера, GPU-пулом RunPods, кредитами, подписками, email-шаблонами, промокодами, релизами.
CORS-конфигурация:
access-control-allow-origin: http://127.0.0.1:3000
access-control-allow-credentials: true
API доверяет localhost:3000 — внутреннему Next.js dev-серверу. Если получить SSRF с этого хоста — можно обойти IP-whitelist.
Раскрытие внутреннего URL бэкенда:
GET /api/config → {"apiHost": "https://api.internal.example.com"}
Почему это проблема
JS-бандлы доступны без аутентификации. Атакующий получает полную структуру admin API: все маршруты, параметры, ролевую модель, имена cookie — не отправив ни одного запроса к защищённым эндпоинтам. Это значительно ускоряет планирование атаки.
Урок: Разделяйте admin-бандлы. Не включайте маршруты dashboard в публичный JS. Используйте server components Next.js для чувствительной логики. И помните: минификация ≠ безопасность.

-
6. Открытый Docker API: как одна ошибка обесценивает всё остальное
После двух недель сложных техник — SSRF через wildcard DNS, CVE в LangChain, HTTP smuggling — мы вернулись к порту, обнаруженному в первый день.
Порт 2375
Docker Remote API. Один HTTP-запрос:
GET /info → {"Containers": 12, "Swarm": {"LocalNodeState": "active"}, ...}
Без аутентификации. Без TLS. Без какой-либо защиты.
Что это означает на практике
Через Docker API доступно чтение метаданных контейнеров, включая переменные окружения:
ANTHROPIC_API_KEY=sk-ant-api03-...
OPENAI_API_KEY=sk-...
JWT_SECRET=your-secret-key-change-in-production
DATABASE_URL=postgresql://user:pass@10.0.0.13/db
REDIS_URL=redis://10.0.0.18:6379/0
Все секреты платформы. В одном запросе.
Помимо чтения, 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 уязвимостей. Вот как они складываются в цепочку:
JWT forgery (дефолтный секрет)
├── Профили и данные пользователей
├── Кластер: все ноды, IP, порты, GPU
└── API-токен
└── Ollama SSRF (nip.io)
├── Подмена ответов LLM (canary injection)
└── Эксфильтрация промптов пользователей
Docker 2375 (без аутентификации)
├── ENV всех контейнеров → ВСЕ API-ключи
├── Произвольные контейнеры → RCE
└── Монтирование файловой системы → чтение любых файлов
Сводка
Уязвимость Severity AI-специфично?
Docker API без auth Critical Нет — классическая инфра-ошибка
JWT дефолтный секрет Critical Нет — классическая ошибка
Ollama SSRF (nip.io) High Да — user-controlled model host
Canary injection High Да — подмена ответов LLM
Ключи plain text в БД High Частично — дорогие LLM-ключи
DMARC p=none Medium Нет
PostHog Hobby в prod Medium Нет
CORS localhost + info disclosure Low Нет

Характерно: самые критичные уязвимости — не 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-маршруты
  • [ ] Регулярный аудит открытых портов

-
Таймлайн аудита
Период Фокус Ключевые находки
День 1-2 Разведка, code review Открытый Docker 2375, JWT дефолтный секрет, SSRF в Ollama
День 3-4 Аутентификация JWT forgery → полный дамп кластера, профилей, кластерных данных
День 5-6 SSRF Ollama SSRF через nip.io, canary injection, зондирование сети
День 7-10 Попытки перехвата ключей 15+ техник (template injection, smuggling, CVE, race) — все неуспешны
День 11-12 Admin-панель Реверс JS-бандлов, cookie auth, CORS, полная карта admin API
День 13 Docker API Чтение ENV — все API-ключи извлечены
День 14 Отчёт Responsible disclosure, ротация ключей
--Если прямо сейчас у вас:
  • docker-compose в проде без ревизии сетевых политик
  • API-ключи в переменных окружения или plain text в базе
  • Нет egress-фильтрации на worker’ах
  • JWT-секрет, который «поменяем потом»
— ваша LLM-платформа уже потенциально уязвима, даже без сложных атак. Не нужны ни SSRF, ни CVE. Достаточно одного открытого порта.
Все уязвимости закрыты. Ключи ротированы. Если строите LLM-платформу — используйте чеклист выше.-Источник
 
Loading...
Error