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

Disclaimer: Всё описанное — результат санкционированного аудита безопасности по договору. Уязвимости ответственно раскрыты, ключи ротированы, домены и IP изменены. Статья — для понимания, не для воспроизведения.

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

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper

Мы искали уязвимости в 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, в обход всей цепочки.

Содержание

  1. Инфраструктура и разведка
  2. Аутентификация: JWT с дефолтным секретом
  3. SSRF через LLM-провайдер: новый класс уязвимости
  4. Охота за API-ключами: что мы пробовали и почему не получилось
  5. Admin-панель: что расскажут JS-бандлы
  6. Открытый Docker API: как одна ошибка обесценивает всё остальное
  7. Итоги, уроки и рекомендации

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-стартапах снова и снова:

  1. Быстрый MVP на docker-compose — переезжает в прод без ревизии
  2. Дефолтные секреты — «поменяем позже» (не поменяют)
  3. Плоская сеть — все сервисы видят друг друга, egress не ограничен
  4. Ключи как строки — plain text в ENV, в БД, в HTTP между сервисами
  5. 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-платформу — используйте чеклист выше.

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper

Почему ваша LLM-платформа — следующая цель: аудит безопасности AI-сервиса изнутри (Гайд и обучение Claude Code)
Начать дискуссию