المعمارية English

Qwizin — معمارية الأمن

الحالة: مسودة v0.1 · التاريخ: 2026-07-20 يرتبط بـ: QAS-SEC-*، QAS-INT-*، QAS-CMP-*، R-03R-07، CON-10 (MoM NFR-1) وثيقة مرافقة: عرض وقت التشغيل (التدفقات) · DevOps (تحصين الـ cluster)

ملاحظة على النطاق. تغطي هذه الوثيقة أمن التطبيق والبيانات. أما تحصين Kubernetes وسلسلة التوريد فمكانه الغوص العميق في DevOps §5. والالتزامات التنظيمية في المخاطر §A — وتبقى غير مُتحقَّق منها بانتظار استشارة قانونية سعودية.


1. حدود الثقة (trust boundaries)#

flowchart TB
    subgraph UNTRUSTED["🔴 Untrusted"]
        MOB["📱 Mobile client<br/><i>attacker-controlled</i>"]
        ANON["🌐 Anonymous internet"]
        UPLOAD["📎 Uploaded files<br/><i>from unverified parties</i>"]
    end

    subgraph SEMI["🟠 Semi-trusted"]
        AUTH["Authenticated user<br/><i>may attack other tenants</i>"]
        EMP["Employee<br/><i>may attack employer/colleagues</i>"]
        EXTW["External webhooks<br/><i>spoofable</i>"]
    end

    subgraph TRUSTED["🟢 Trusted — inside the boundary"]
        API["API tier"]
        WORK["Workers"]
        DB[("PostgreSQL + RLS")]
        OBJ[("Object Storage")]
    end

    subgraph PRIV["🔵 Privileged"]
        ADM["🛡️ Admin portal<br/><i>highest-value target</i>"]
    end

    MOB & ANON -->|"TB-1: TLS · WAF · rate limit<br/>authn · input validation"| API
    AUTH -->|"TB-2: authz · tenant scope · RLS"| API
    EMP -->|"TB-3: employer/colleague isolation"| API
    UPLOAD -->|"TB-4: quarantine · magic bytes · AV"| OBJ
    EXTW -->|"TB-5: signature verify · idempotency"| API
    ADM -->|"TB-6: MFA · separate origin · full audit"| API
    API --> DB & OBJ
    WORK -->|"TB-7: re-establish tenant context"| DB

    style UNTRUSTED fill:#fde8e8,stroke:#c94a4a
    style SEMI fill:#fff4e5,stroke:#d98c1f
    style TRUSTED fill:#e8f5e9,stroke:#2d8659
    style PRIV fill:#e8f0fe,stroke:#1168bd
المعرّف الحد التهديد الأساسي التفصيل
TB-1 الإنترنت ← API الحقن (injection)، هجمات بيانات الاعتماد، هجوم التعداد §3
TB-2 مستخدم مُصادَق ← البيانات الوصول العابر للمستأجرين (BOLA) §2
TB-3 موظف ↔ صاحب العمل/الزميل محور العزل الثاني §2.4
TB-4 الرفع ← التخزين البرمجيات الخبيثة، stored XSS، IDOR §4
TB-5 webhook خارجي ← API تأكيد دفع منتحَل §5
TB-6 المسؤول ← كل شيء اختراق الحساب = اختراق المنصة §6
TB-7 مهمة في الطابور ← البيانات سياق المستأجر غائب افتراضيًا §2.3

TB-3 وTB-7 هما الحدّان اللذان تغفل عنهما الفرق روتينيًا. TB-3 لأن «المستأجر» يُنمذَج كمحور واحد مسطّح بينما هما في الحقيقة محوران. وTB-7 لأن المهام ليس لها request، فكل آلية أمان مرتبطة بنطاق الطلب تتبخّر بصمت.


2. عزل المستأجرين (tenant isolation) — الضابط الأعلى أولوية#

QAS-SEC-01 · R-05 · تفصيل الآلية في Flow 6

2.1 لماذا يفشل التحديد على مستوى التطبيق وحده#

التصميم المُغري هو فحص عند كل endpoint: $employee->company_id === $user->company_id. وهو يفشل بنيويًا — فهو صحيح فقط إذا تذكّره كل مطوّر عند كل endpoint، إلى الأبد. أول فحص مَنسي هو اختراق، وسيُنسى، ولا شيء يُظهر هذا الإغفال. لا رسالة خطأ، لا انهيار، فقط الشركة A تقرأ سجلات موظفي الشركة B.

الـ global scope في Eloquent أفضل، لكنه يظل قابلًا للتجاوز عبر withoutGlobalScopes()، والاستعلامات الخام، وDB::table()، وبعض مسارات العلاقات، وأي شخص لا يعلم بوجوده أصلًا.

2.2 خمس طبقات، مرتّبة بحسب تأخّر كل منها في الالتقاط#

# الطبقة هل تفشل بالرفض (fails closed)؟ الدور
1 PostgreSQL RLS مع FORCE ROW LEVEL SECURITY نعم خط الدفاع الأخير. يُرجع صفر صفوف مهما كانت أخطاء التطبيق.
2 TenantContext الذي يرمي استثناءً عند عدم ضبطه ✅ نعم لا يُرجع null أبدًا، ولا يتحوّل افتراضيًا إلى «بلا تصفية»
3 Eloquent global scope ❌ لا راحة في الاستخدام واستفادة من الفهارس — وليس حدًّا أمنيًا
4 اختبار معماري: كل جدول فيه company_id يستخدم الـ trait ✅ عند البناء يلتقط الـ model الذي سيُضاف العام القادم
5 حزمة اختبارات CI هجومية ✅ عند البناء تختبر السلوك المُلاحَظ، لا التفاصيل الداخلية

FORCE ROW LEVEL SECURITY مهم: بدونه يتجاوز مالك الجدول السياسات، ومالك الجدول عادةً هو دور التطبيق نفسه.

الطبقتان 1 و5 تصمدان أمام تغيّر فريق العمل. أما الطبقات 2–4 فتجعل الصواب سهلًا.

2.3 ⚠ نمطا فشل RLS#

كلاهما حالة تكون فيها السياسة مكتوبة بشكل صحيح ومع ذلك تتسرّب البيانات.

flowchart TB
    subgraph F1["Failure 1 — connection reuse"]
        R1["Request A<br/>company=1"] --> C1["Pooled connection<br/>SET app.company_id = 1"]
        C1 --> R2["Request B<br/>company=2"]
        R2 --> LEAK1["🚨 GUC still = 1<br/>Company 2 sees Company 1's data"]
    end

    subgraph F2["Failure 2 — queued job"]
        J1["Job dequeued"] --> J2["No HTTP request<br/>→ no GUC set"]
        J2 --> LEAK2["🚨 Unscoped query<br/>or silent empty result"]
    end

    style LEAK1 fill:#fde8e8,stroke:#c94a4a
    style LEAK2 fill:#fde8e8,stroke:#c94a4a
الفشل التخفيف
إعادة استخدام الاتصال — تجميع الاتصالات بنمط transaction أو الـ workers الدائمة تحمل متغير الجلسة إلى الطلب التالي اضبطه بنطاق المعاملة (transaction-scoped) داخل معاملة صريحة، وأعد ضبطه في terminating middleware. هذا هو ناقل التسرّب الفعلي — السياسة تعمل بشكل مثالي ومع ذلك تُقدَّم بيانات خاطئة.
المهام في الطابور بلا request سلسِل معرّف المستأجر داخل حمولة المهمة؛ واضبطه في job middleware؛ والمهمة التي تعجز عن تأسيس السياق يجب أن ترمي استثناءً، لا أن تتحوّل إلى قيمة افتراضية

هذا سبب رئيسي لتأجيل Laravel Octane. فالـ workers الدائمة في Octane تجعل نمط الفشل الأول أكثر احتمالًا بكثير، مقايِضةً أعلى سمة جودة في النظام مقابل أداء لا يحتاجه حجم الإطلاق.

2.4 محور العزل الثاني#

يُدخل ACT-EMP بُعدًا للعزل يتجاوز شركة↔شركة:

  • يجب ألا يقرأ الموظف درجات تقييم زميله
  • وصول مسؤول الشركة إلى سجلات الموظفين ينبغي أن يكون محدَّد النطاق ومُدقَّقًا، لا مطلقًا
  • شهادات الموظف الشخصية يُحتجّ بأنها ملكه هو لا ملك صاحب العمل (OQ-07)

لا تُنمذِج هذا كحدّ مستأجر واحد مسطّح. التحديد على مستوى الشركة ضروري وغير كافٍ.


3. الهوية والمصادقة#

التدفق في Flow 1.

3.1 استراتيجية الرموز (tokens)#

قيد مُتحقَّق منه: Laravel Sanctum لا يملك آلية refresh token أصلية. فهو يوفّر انتهاء صلاحية الرموز (إعداد expiration، صلاحية لكل رمز، sanctum:prune-expired) والإبطال بحذف الصف — أما التدوير وكشف إعادة الاستخدام فيجب بناؤهما.

الضابط المواصفة
Access token محدَّد بالصلاحيات (ability-scoped)، ≤ 15 دقيقة
Refresh token مُجزَّأ (hashed) عند التخزين، يُستخدم مرة واحدة، ويُدوَّر عند كل استخدام
كشف إعادة الاستخدام إعادة تشغيل refresh token مُستهلَك ← إبطال عائلة الرموز بأكملها + تنبيه
ربط الجهاز زوج رموز واحد لكل جهاز مُسمّى ← تجربة «تسجيل الخروج من هذا الجهاز» حقيقية
مُحفّزات الإبطال تغيير كلمة المرور، رفض/تعليق الحساب، إجراء صريح من المستخدم، تعليق الشركة بالتتابع
التخزين على العميل iOS Keychain / Android Keystore — وليس أبدًا AsyncStorage أو SharedPreferences

كشف إعادة الاستخدام هو أعلى ضوابط المصادقة قيمةً على الإطلاق. فهو يحوّل سرقة الرمز من وصول صامت دائم إلى حادث مكتشَف.

ليس JWT عديم الحالة (stateless): الإبطال هنا متطلب وظيفي — رفض شركة يجب أن يقطع الوصول عن موظفيها فورًا. والرموز عديمة الحالة تجعل ذلك صعبًا.

3.2 تسجيل الدخول عبر Google#

تحقّق من ID token على الخادم: التوقيع مقابل JWKS الخاصة بـ Google، إضافةً إلى aud وiss وexp والـ nonce. لا تثق أبدًا ببريد إلكتروني أو ملف تعريف يرسله العميل.

⚠️ الاختطاف المسبق عبر ربط الحسابات (account-linking pre-hijacking). الربط التلقائي لتسجيل دخول Google بحساب قائم عبر مطابقة البريد الإلكتروني قابل للاستغلال إذا لم يُتحقَّق من ذلك البريد قط: يسجّل المهاجم بالبريد الإلكتروني للضحية، ثم يسجّل الضحية لاحقًا الدخول عبر Google، فيندمج الحسابان في وصول يتحكم فيه المهاجم. اشترط التحقق من بريد الحساب القائم قبل الربط.

3.3 OTP / الرسائل النصية#

الضابط المبرّر
مُجزَّأ عند التخزين، مقارنة بزمن ثابت، استخدام واحد، صلاحية قصيرة معياري
تحديد المعدّل لكل رقم هاتف، وكل IP، وكل جهاز؛ تراجع أُسّي (exponential backoff)
قائمة سماح للدول (country allowlisting)
إثبات سلامة التطبيق (Play Integrity / App Attest)
الحصص كضابط تكلفة، لا ضابط أمني فقط احتيال ضخّ الرسائل (SMS pumping) ناقل خسارة مالية مباشرة — يستنزف المهاجم ميزانية الرسائل دون أن ينشئ حسابًا أصلًا

⚠️ غير مُتحقَّق منه: متطلبات تسجيل مُعرّف المُرسِل (sender ID) في السعودية لدى الجهة المنظِّمة للاتصالات. تأكّد قبل اختيار مزوّد.

3.4 معمارية التخويل#

ثلاثة اهتمامات منفصلة يجب ألا تتشارك آلية واحدة:

flowchart LR
    REQ["Request"] --> S1{"1. Account state<br/><i>precondition</i>"}
    S1 -->|"not approved"| ALLOW{"On the explicit<br/>allowlist?"}
    ALLOW -->|No| DENY1["❌ 403 + machine-readable state"]
    ALLOW -->|Yes| S2
    S1 -->|"approved"| S2{"2. Capability<br/><i>may this role ever?</i>"}
    S2 -->|No| DENY2["❌ 403"]
    S2 -->|Yes| S3{"3. Resource policy<br/><i>this record, now?</i>"}
    S3 -->|No| DENY3["❌ 404 for cross-tenant"]
    S3 -->|Yes| OK["✅ Proceed"]

    style DENY3 fill:#fff4e5,stroke:#d98c1f

حالة الحساب شرط مسبق على الجلسة، لا صلاحية. نمذجتها كصلاحية تعني أن كل endpoint جديد يكون غير محمي افتراضيًا — وهو عكس المطلوب في منصة قائمة على بوابة اعتماد. طبّقها كـ قائمة سماح صريحة: كل شيء مرفوض أثناء الانتظار عدا قائمة قصيرة قابلة للمراجعة (عرض الملف الشخصي، رفع المستندات، إعادة التقديم، تصفح الـ marketplace العام، تسجيل الخروج).

⚠️ مزالق مُوثَّقة في إطار العمل — الثلاثة حقيقية والثلاثة سهلة الوقوع فيها:

  1. خطّاف before عام يُرجع false يُعطّل كل policy في التطبيق. فأي قيمة غير null تُعامَل كنتيجة التخويل النهائية، وتُقصِر كل ما بعدها — بما في ذلك قدرة المسؤول على مراجعة الحسابات المعلّقة. يجب أن يُرجع null في المسار الطبيعي. احصر الخطّاف العام في ترقية صلاحيات الـ super-admin فقط.
  2. مساعدات التخويل السطرية (inline) تتخطى خطّافات before/after كليًا. وأي استدعاء منها يفلت بصمت من فحص حالة الحساب العام. امنعها خارج خدمة الصلاحيات (capability service).
  3. دالة before() الخاصة بالـ policy لا تُستدعى إذا لم تكن الـ policy تحوي دالة مطابقة للصلاحية. ولذلك فإن نهج الـ base policy له نقطة عمياء تجاه الصلاحيات التي لا تُنفّذها الـ policy المُحدَّدة.

أرجِع 404 لا 403 عند محاولات استكشاف موارد عابرة للمستأجرين — فالـ 403 يؤكد وجود المورد ويُمكّن هجوم التعداد.


4. مسار رفع المستندات#

TB-4 · QAS-SEC-02، QAS-SEC-03 · R-07 · التدفق في Flow 2

التهديد الذي يُعرِّف هذا الـ pipeline: الـ onboarding يقبل ملفات عشوائية من أطراف غير مُتحقَّق منها — بحكم التعريف، لأن التحقق هو الغاية من الرفع أصلًا. وحمولة stored XSS تُقدَّم من نفس الأصل (same-origin) تُنفَّذ ضد جلسة المسؤول المُراجِع، وهي أعلى الجلسات صلاحيةً في النظام.

flowchart LR
    U["Upload"] --> V1["Size · count · declared type"]
    V1 --> V2["🔍 <b>Magic-byte inspection</b><br/><i>never extension or client MIME</i>"]
    V2 --> V3["Server-generated filename<br/><i>never the user's</i>"]
    V3 --> Q[("🔒 Quarantine bucket<br/>encrypted · no admin read")]
    Q --> AV["ClamAV<br/><i>separate Deployment</i>"]
    AV -->|"infected"| REJ["❌ Rejected<br/>admin never sees it"]
    AV -->|"clean"| PROM[("✅ Documents bucket")]
    PROM --> SIGN["Signed URL, TTL ≤ 5 min<br/>issued AFTER authz"]
    SIGN --> SEP["Separate domain<br/>Content-Disposition: attachment<br/>X-Content-Type-Options: nosniff"]

    style Q fill:#fde8e8,stroke:#c94a4a
    style REJ fill:#fde8e8,stroke:#c94a4a
    style PROM fill:#e8f5e9,stroke:#2d8659
الثابت (invariant) السبب
الحجر (quarantine) أولًا، ثم الترقية عند النظافة يجب ألا يكون المسؤول المُراجِع هو آلية كشف البرمجيات الخبيثة
فحص البايتات الأولى (magic bytes)، لا الامتدادات كلٌّ من الامتداد ونوع MIME المُعلَن من العميل يتحكم بهما المهاجم
أسماء ملفات يولّدها الخادم أسماء ملفات المستخدم تحمل اجتياز المسارات (path traversal) وبايتات فارغة وحيل Unicode
نطاق منفصل — لا نطاق فرعي يتشارك الـ cookies محتوى المستخدم من نفس الأصل هو stored XSS ضد جلسة المسؤول
روابط موقَّعة (signed URLs) قصيرة الـ TTL تُصدَر بعد التخويل يَحدّ من التعرّض عند تسرّب الرابط (QAS-SEC-02)
الرفض عند الشك (fail closed) تعطّل الماسح = لم يثبت أنه نظيف بعد، وليس أبدًا افترضه نظيفًا
إصدارات غير قابلة للتعديل مسار تدقيق يشير إلى مستند جرى تعديله لا يُثبت شيئًا (QAS-CMP-02)
معاينة مُسطَّحة (flattened) للمراجعة ملفات PDF تحمل JavaScript مضمّنًا؛ وملفات SVG تحمل XXE؛ وملفات polyglot موجودة. يجب ألا يفتح المُراجِعون النسخة الأصلية في محرك PDF داخل المتصفح.

يعمل ClamAV كـ Deployment منفصل، لا كـ sidecar داخل كل pod — فقاعدة بيانات التواقيع بحجم ~1 GB وستُستنسخ لولا ذلك داخل كل pod تطبيقي.

التشفير عند التخزين — على مستوى التطبيق، فوق تشفير التخزين#

موصى به للمستندات القانونية تحديدًا. فالتشفير على مستوى التخزين يحمي فقط من السرقة المادية للوسائط. وهو لا يحمي من بيانات اعتماد تطبيق مخترقة، أو bucket مُهيّأ خطأً، أو مشغّل مُفرَط الصلاحيات.

استخدم التشفير المغلَّف (envelope encryption)، لا مُشفِّر النصوص في إطار العمل — فتلك الواجهة تُحمِّل الملفات كاملةً في الذاكرة وهي خاطئة لملفات PDF بحجم عدة ميغابايت:

per-file random DEK → stream-encrypt AES-256-GCM
DEK wrapped by a KMS-held KEK → stored alongside the object

المفاتيح لكل ملف تعزل نطاق الضرر؛ ولا يعبر حدود الـ KMS سوى 32 بايت، ما يتجنب الكمون والتكلفة لكل بايت.

اذكر التكلفة بصراحة: تكامل KMS، وأدوات تدوير المفاتيح، ومهمة إعادة التغليف، وعناية دقيقة بنظافة الـ DEK في الذاكرة (لا يُسجَّل أبدًا، ولا يُحفظ مفكوك التشفير)، وفقدان دائم للبيانات إذا فشلت عهدة المفاتيح. ارصد ميزانية النضج التشغيلي، لا ميزانية الكود وحده.


5. التكاملات الخارجية#

TB-5

الضابط ينطبق على
تحقّق من التوقيع تزامنيًا، قبل الإدراج في الطابور جميع الـ webhooks
idempotency على معرّف الحدث لدى المزوّد جميع الـ webhooks — المزوّدون يعيدون المحاولة
لا تثق أبدًا بنجاح دفع يُبلّغ عنه العميل الفوترة
التسوية (reconciliation) على الخادم وفق جدول زمني الفوترة، الفيديو
حصر الخروج (egress) في نطاقات المزوّدين المعروفة الكل

التسوية ليست اختيارية. فالـ webhooks عرضة للفقد. والفوترة مبنية على مدة الجلسة — وSessionEnded مفقود يعني خطأ في الفوترة أو جلسة مدفوعة مفتوحة إلى أجل غير مسمّى. يجب أن تقارن مهمة مجدولة حالة المزوّد بالحالة المحلية وتُغلق الجلسات اليتيمة.


6. بوابة المسؤول (Admin Portal)#

TB-6الهدف الأعلى قيمةً في النظام. يستطيع مسؤول المنصة اعتماد الحسابات، وقراءة المستندات القانونية لكل مستأجر، ولمس المدفوعات. حساب مسؤول واحد مخترَق ≈ اختراق المنصة.

الضابط المتطلب
MFA إلزامي، بلا استثناءات
أصل منفصل (separate origin) ليس مسارًا على النطاق الموجّه للمستخدمين
مصادقة مُعزَّزة (step-up) مطلوبة لـ: اعتماد المستندات، إصدار/إبطال الشهادات، المبالغ المستردة، وتغيير بيانات الحساب البنكي للـ payout
تدقيق كامل كل إجراء، مع الفاعل وعنوان IP والطابع الزمني
انتحال هوية المستخدم (impersonation) مُسجَّل صراحةً، محدود بمدة زمنية، ومُعلَّم بوضوح داخل الجلسة
تقييد IP حيثما كان ممكنًا تشغيليًا

تغييرات بيانات الـ payout تستحق فقرة خاصة بها. الاستيلاء على الحساب ← إعادة توجيه الـ payouts هو احتيال الـ marketplace الكلاسيكي. المطلوب: step-up MFA، ونافذة تأخير قبل سريان التغيير، وإشعار خارج القناة (out-of-band) إلى قناة التواصل السابقة — بما يمنح المالك الشرعي فرصة للتدخل.


7. سلامة التقييمات والشهادات#

QAS-INT-01/02/03 · R-06 · التدفق في Flow 4

لماذا يُصنَّف هذا كاهتمام أمني لا كاهتمام صحّة بيانات: هذه الشهادات تُقرّ بكفاءة في سلامة الغذاء. والشهادة المزوّرة تنقل خطرًا واقعيًا إلى روّاد المطاعم وتدمّر ادّعاء القيمة المركزي للمنصة.

7.1 سرية مفتاح الإجابات#

القاعدة مطلقة: الإجابة الصحيحة لا تغادر الخادم أبدًا — لا كعَلَم (flag)، ولا في البيانات الوصفية، ولا قابلةً للاستنتاج من ترتيب الخيارات أو ترتيب مفاتيح الكائن.

مسار التسرّب الضابط
الإفراط في التسلسل عبر ORM — model يُسلسَل عبر toArray() يُرسل correct_option_id موارد API صريحة بـ قائمة سماح للحقول، إضافةً إلى $hidden كدفاع في العمق. وهذا مسار اختراق شائع جدًا في الواقع.
نقطة نهاية المراجعة — إظهار إجاباته الخاطئة للمتعلّم بشكل مشروع، وإعادة كشف المفتاح كاملًا في الوقت نفسه قيّدها؛ ولا تكشف المفاتيح إلا للأسئلة التي جرت محاولتها فعليًا
التصحيح على العميل من أجل «التغذية الراجعة الفورية» أعِد كل إجابة إلى الخادم؛ والخادم هو من يقرر ما يُكشف

اختبار عقد (contract test) في CI يتحقق من غياب حقول الصحة من مخطط التسليم.

7.2 سلامة الدرجات#

التهديد الضابط
العميل يرسل درجة مزوّرة لا وجود لحقل score في الـ API. الإرسال يحمل الإجابات فقط.
تجاوز الحد الزمني يُفرض مقابل وقت البدء المسجَّل على الخادم، لا ساعة العميل أبدًا
سباق إعادة الإرسال / التصحيح المزدوج مفتاح idempotency؛ والإرسال الثاني لمحاولة مُنجَزة يُرفض، لا يُعاد تصحيحه
إعادة المحاولة حتى النجاح، ثم عرض الناجحة فقط جميع المحاولات إلحاقية فقط (append-only) ومحفوظة؛ وحدود المحاولات قابلة للتهيئة
تبادل ورقة الإجابات تجميع الأسئلة (سحب N من M) + ترتيب عشوائي للخيارات في كل محاولة
تعديل الدرجة بأثر رجعي غير قابلة للتعديل بعد الإنجاز؛ والتصحيحات فقط عبر مسار منفصل مُدقَّق مقصور على المسؤول

7.3 مقاومة العبث في الشهادات — ثلاث طبقات مستقلة#

flowchart LR
    C["Certificate PDF"] --> L1["1️⃣ Signed QR payload<br/><i>cert id · holder · course ·<br/>issue · expiry + signature</i><br/><b>→ offline verification</b>"]
    C --> L2["2️⃣ Public verification endpoint<br/><i>/verify/{ULID}</i><br/><b>→ revocation + current status</b>"]
    C --> L3["3️⃣ Digitally signed PDF (PAdES)<br/><i>requires a trust service provider</i><br/><b>→ strongest for third parties</b>"]

كل طبقة تهزم مهاجمًا مختلفًا:

  1. حمولة QR موقَّعة. الشهادة المزوّرة بصريًا تفشل في فحص التوقيع حتى بدون أي اتصال بالشبكة — وهو أمر مهم لمفتش بلدية داخل مطبخ. استخدم مفتاح توقيع مخصصًا، لا مفتاح التطبيق.
  2. نقطة نهاية تحقق عامة. استخدم ULID أو رمزًا عشوائيًا بطول 128 بت، ولا تستخدم أبدًا رقم شهادة تسلسليًا — فالمعرّفات التسلسلية تتيح لأي شخص تعداد مجموعة الشهادات بأكملها. اكشف الحد الأدنى من البيانات الشخصية (PII): الدورة، تاريخا الإصدار والانتهاء، صالحة/مُبطَلة، وعلى الأكثر جزء من اسم حاملها. وصفحة عامة تُرجع الاسم الكامل مع رقم الهوية الوطنية هي خرق لـ PDPL بفعل ذاتي. وحدِّد معدل الطلبات عليها.
  3. توقيع PAdES. ⚠️ غير مُتحقَّق مما إذا كان الحصول على شهادة توقيع موثوقة سعوديًا ممكنًا — ويستحق البحث، لأن التوقيع الموثوق محليًا يحمل وزنًا أكبر لدى الجهات التنظيمية السعودية.

يجب أن يوجد الإبطال من اليوم الأول. فالشهادات تُصدر بالخطأ، وتُصدر لحسابات يتبيّن لاحقًا أنها احتيالية، وتنتهي صلاحيتها. نقطة نهاية التحقق هي آلية الإبطال — وهذا بالضبط سبب وجوب أن يرتبط رمز QR بها بدلًا من أن يكون مكتفيًا بذاته فقط. صمّم الحمولة غير المتصلة بحيث تحمل تاريخ انتهاء لتتدهور بأمان.


8. سجل التدقيق#

QAS-CMP-02

ما يجب تسجيله#

أحداث المصادقة (النجاح، الفشل، MFA، تغيير بيانات الاعتماد) · كل حالات رفض التخويل (إشارة الهجوم) · رفع/اعتماد/رفض المستندات مع هوية المُراجِع · محاولات التقييم وتصحيحات الدرجات · إصدار الشهادات وإبطالها · كل الأحداث المالية · تغييرات بيانات الـ payout · انتحال هوية المستخدم من قبل المسؤول · تغييرات الأدوار · كل تصدير أو قراءة جماعية للبيانات الشخصية (ذات صلة بـ PDPL).

السلامة#

تخلق سير عمل الاعتماد والمعاملات المالية خطر الإنكار (repudiation)، ولذلك فإن جدول سجل عادي قابل للتعديل غير كافٍ — فأي شخص يملك صلاحية الكتابة على قاعدة البيانات يستطيع إعادة كتابة التاريخ.

استخدم سجلًا إلحاقيًا فقط بسلسلة تجزئة (hash-chained ledger):

chain_hash(n) = SHA256( chain_hash(n-1) || payload_hash(n) )

مع نقاط تفتيش موقَّعة (signed checkpoints) دوريًا تُثبِّت رأس السلسلة.

وكمّله بحماية على مستوى قاعدة البيانات: اسحب صلاحيتي UPDATE وDELETE على جدول التدقيق من دور التطبيق، وأرسل السجلات إلى تخزين خارجي يُكتب مرة واحدة (write-once).

نظافة البيانات الشخصية#

احجب البيانات في طبقة التسجيل، لا بانضباط المطوّرين.

لا يُسجَّل أبدًا دائمًا
أرقام الهوية الوطنية، رموز OTP، الرموز (tokens)، محتويات المستندات، بيانات البطاقات سجّل المعرّفات، لا القيم
أجسام الطلبات كاملةً في تقارير الاستثناءات هيّئ تقارير الاستثناءات لتنظيف حمولات الطلبات — فأثر مكدّس (stack trace) مع جسم طلب كامل هو تسريب روتيني للبيانات الشخصية
APP_DEBUG=false في الإنتاج، مُتحقَّق منه بفحص آلي

⚠️ تعارض في مدد الاحتفاظ. الاحتفاظ بسجل التدقيق (≥ 7 سنوات للتوافق المالي) يتعارض مع مبدأ تقليل البيانات وحقوق المحو في PDPL. احسم هذا عمدًا مع مستشار قانوني ووثّقه في سجل أنشطة المعالجة — لا تَرتجل عند وصول طلب حذف.


9. نموذج التهديدات STRIDE#

أعلى التهديدات خطورةً بحسب الحد.

الحد التهديد (STRIDE) الخطورة الضابط
TB-2 Elevation (تصعيد الصلاحيات) — وصول عابر للمستأجرين (BOLA) 🔴 حرج RLS + ربط محدَّد النطاق + حزمة اختبارات CI هجومية (§2)
TB-4 Tampering/Elevation (العبث/تصعيد الصلاحيات) — رفع ملف خبيث ← stored XSS ضد المسؤول 🔴 حرج أصل منفصل، magic bytes، quarantine + AV (§4)
TB-4 Info disclosure (كشف المعلومات) — IDOR على تراخيص شركة أخرى 🔴 حرج signed URLs قصيرة الـ TTL بعد التخويل؛ ULIDs
TB-6 Elevation — اختراق المسؤول ← وصول كامل لكل المستأجرين 🔴 حرج MFA، أصل منفصل، step-up، تدقيق كامل (§6)
TB-1 Tampering — درجة يرسلها العميل / مفتاح إجابات داخل الحمولة 🟠 عالٍ تصحيح على الخادم؛ موارد صريحة (§7)
TB-1 Spoofing (الانتحال) — refresh token مسروق 🟠 عالٍ التدوير + كشف إعادة الاستخدام ← إبطال العائلة (§3.1)
TB-1 Info disclosure — تسلسل مُفرط الاتساع يسرّب correct_option_id وأرقام الهوية الوطنية 🟠 عالٍ موارد صريحة، $hidden، اختبارات شكل الاستجابة
Offline Spoofing — شهادة سلامة غذاء مزوّرة 🟠 عالٍ (سلامة في العالم الواقعي) QR موقَّع + نقطة نهاية تحقق + إبطال (§7.3)
TB-5 Tampering — webhook مزوّر يؤكد معاملة غير مدفوعة 🟠 عالٍ التحقق من التوقيع؛ لا تثق أبدًا بنجاح يبلّغ عنه العميل (§5)
TB-5 Elevation — إعادة توجيه الـ payout عبر الاستيلاء على الحساب 🟠 عالٍ step-up MFA + تأخير + تنبيه خارج القناة (§6)
TB-7 Info disclosure — مهمة تعمل بلا سياق مستأجر 🟠 عالٍ السياق من الحمولة؛ ارمِ استثناءً عند غيابه (§2.3)
TB-1 Info disclosure — استشارات مسجَّلة تحتوي بيانات صحية 🟠 عالٍ تخزين داخل المملكة؛ روابط تشغيل موقَّعة؛ OQ-08/R-04
TB-2 Info disclosure — SQLi عبر تعبيرات خام تتجاوز نطاق المستأجر 🟠 عالٍ استعلامات مُعامَلة (parameterised)؛ منع SQL الخام في مسارات المستأجرين؛ دور قاعدة بيانات بأقل الصلاحيات
Ledger Repudiation (الإنكار) — payout أو اعتماد متنازَع عليه 🟡 متوسط سجل تدقيق بسلسلة تجزئة (§8)

الأربعة التي تُعالَج أولًا: BOLA العابر للمستأجرين · IDOR على المستندات · الرفع الخبيث · اختراق المسؤول. كل واحد منها قادر بمفرده على كشف بيانات كل عميل.


10. أولوية التنفيذ#

مرتّبة بحسب تخفيض المخاطر لكل ساعة عمل، لا بحسب الطرافة التقنية.

الأسبوع 1 — أعلى نسبة، وأقل خطر تعطيل#

  1. APP_DEBUG=false / APP_ENV=production مُتحقَّق منهما بفحص آلي
  2. تأكيد أن تدفق الدفع مُستضاف/بإعادة توجيه بحيث لا تلمس بيانات البطاقات بنية Qwizin التحتية أبدًا
  3. إخراج الأسرار النصية من إدارة الإصدارات؛ فحص الأسرار في CI؛ وتدوير أي سرّ سبق إيداعه
  4. التصحيح على الخادم حصرًا؛ موارد API صريحة بقوائم سماح للحقول
  5. تغيير كلمة المرور ← إبطال كل عائلات الرموز

الشهر 1 — بنيوي#

  1. سياسات RLS جنبًا إلى جنب مع أول migration محدَّد بالمستأجر — فتركيبها لاحقًا على جدول مليء بالبيانات يستلزم نافذة صيانة
  2. TenantContext الذي يرمي استثناءً؛ متغير جلسة بنطاق المعاملة + إعادة ضبط في terminating middleware
  3. إعادة تأسيس سياق المستأجر في كل مهمة في الطابور، مع رمي استثناء عند غيابه
  4. حزمة اختبارات CI هجومية للوصول العابر للمستأجرين — الضابط الذي يحفظ سلامتك فعليًا مع مرور الوقت
  5. مسار الرفع: quarantine ← AV ← ترقية؛ نطاق منفصل؛ magic bytes
  6. تدوير رمز التحديث مع كشف إعادة الاستخدام
  7. MFA للمسؤول + أصل منفصل + تغطية تدقيق
  8. تشفير مغلَّف على مستوى التطبيق للمستندات القانونية

الربع 1 — العمق#

  1. سجل تدقيق بسلسلة تجزئة مع نقاط تفتيش موقَّعة
  2. QR موقَّع للشهادة + نقطة نهاية تحقق عامة + إبطال
  3. مصادقة step-up على تغييرات الـ payout، مع تأخير وإشعار خارج القناة
  4. حجب البيانات في طبقة التسجيل؛ تقارير استثناءات مُنظَّفة
  5. جرد البيانات الشخصية — مطلوب حتى يكون QAS-CMP-01 قابلًا للتحقق أصلًا

بخصوص جرد البيانات الشخصية: الجزء الصعب في طلب صاحب البيانات ليس الحذف، بل الحصر. فبمجرد أن تنتشر البيانات داخل فهرس بحث، و projection للتقارير، و pipeline للسجلات، يصبح سؤال «اعثر على كل شيء عن هذا الشخص» بلا إجابة دون جرد يُصان كأصل من الدرجة الأولى. ابنِه بينما النظام لا يزال صغيرًا.


11. حيث يلزم مستشار قانوني، لا بحث#

⚠️ هذه ليست أسئلة هندسية. التفصيل في المخاطر §A.

  1. ترخيص ساما SAMA (R-01) — قبل بناء الـ payouts
  2. الفوترة الإلكترونية ZATCA (R-02) — النطاق، ومسألة مورّد السجل (supplier-of-record) في الـ marketplace
  3. الواجبات التشغيلية لـ PDPL (R-03) — مُهَل الإبلاغ عن الخروقات، ومواعيد الاستجابة لطلبات أصحاب البيانات، وعتبة تعيين مسؤول حماية البيانات (DPO)، وتعارض مدد الاحتفاظ مع الاحتفاظ بسجل التدقيق، والأساس النظامي لـ وصول صاحب العمل إلى سجلات تدريب الموظفين، وما إذا كانت الاستشارات المسجَّلة تُعدّ بيانات صحية حساسة

لا يرد في هذه الوثيقة أي موعد نهائي للإبلاغ عن خرق ولا أي زمن استجابة لحقوق أصحاب البيانات، لأن تلك بالتحديد هي الأرقام التي ستُرمَّز داخل كتيّبات التشغيل واتفاقيات مستوى الخدمة — ولم يتسنَّ التحقق منها مقابل مصادر أولية. غيابها متعمّد بدلًا من تخمينها.

القائد التقني · أمير هارون مسودة v0.1 · بحث وتصميم فقط