> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أمن ال API بالأمثلة مع OWASP API Security Top 10
- URL: https://arabroot.io/articles/امن-واجهات-api-مع-owasp-api-security-top-10/
- Published: 2026-10-06T03:00:00.000Z
- Updated: 2026-10-09T09:43:25.000Z
- Description: أشهر عشرة مخاطر في واجهات API كما ترتبها OWASP، ولكل خطر مثال لكود ضعيف وكيف تصلحه وتختبره.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك تطبيق جوال لمتجر صغير، يعرض للعميل طلباته فقط، ويخفي زر لوحة الإدارة عن المستخدم العادي، ولا يعرض في صفحة الملف الشخصي إلا الاسم ورقم الجوال. والتطبيق في الحقيقة واجهة جميلة فوق واجهة برمجة تطبيقات API على الإنترنت، وكل ما يخفيه التطبيق عن المستخدم ما زال موجوداً في ال API نفسه، فأي شخص يستطيع أن يأخذ التوكن Token من جواله ويرسل الطلبات بأداة curl، فيغير رقم الطلب في الرابط، أو يستدعي رابط الإدارة مباشرة، أو يقرأ حقولاً في الرد لا يعرضها التطبيق. ولذلك فأمن ال API لا يقاس بما تعرضه الواجهة، وإنما بما يقبله السيرفر ويرده، ولهذه المشكلات قائمة خاصة من مؤسسة OWASP اسمها **OWASP API Security Top 10**.

وقد رسمنا في مقال [OWASP Top 10 للمبتدئين](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) الخريطة العامة لأمن تطبيقات الويب، وذكرنا في قسم [القائمة وأقاربها](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#family) أن لواجهات API قائمة مستقلة، وهذا المقال هو تلك القائمة بالتفصيل، على API واحد صغير للطلبات، ولكل خطر فيه مثال ضعيف Vulnerable ومثال مصلح Fixed بلغات C# وJava وPython وJavaScript، ثم تجربة حقيقية نرى فيها خمساً من الثغرات بأعيننا ثم نصلحها ونكتب لها اختبارات آلية. والهدف دفاعي تعليمي، أي أن تعرف كيف تمنع كل خطر في الكود الذي تكتبه أنت، وكيف تكتشفه إذا فاتك.

وسوف نناقش في هذا المقال ما يلي:

- ما هي قائمة OWASP API Security Top 10، وكيف بنيت نسخة 2023، وهل صدرت نسخة أحدث منها.
- ما الذي يجعل ال API مختلفاً عن صفحة الويب، ولماذا لا تحمي الواجهة أي شيء.
- المخاطر العشر واحداً واحداً على API للطلبات، مع الكود الضعيف والمصلح، وطرق المنع Prevention والاكتشاف Detection.
- تجربة عملية Lab فيها BOLA وBOPLA وBFLA واستهلاك الموارد وSSRF، بمخرجات curl حقيقية قبل الإصلاح وبعده، واختبارات صلاحيات آلية تفشل ثم تنجح.
- بوابات ال API Gateway، وملف OpenAPI كعقد Contract وسجل Inventory، والتحقق من المخطط Schema Validation، وترقيم الصفحات Pagination، والنسخ Versioning والإيقاف Deprecation، ثم GraphQL وال Webhooks باختصار.
- الأخطاء الشائعة، وقائمة تحقق Checklist لكل Endpoint جديد.

## ما هي قائمة OWASP API Security Top 10، وهل صدرت نسخة أحدث من 2023؟

القائمة وثيقة توعية Awareness Document ينشرها [مشروع OWASP API Security](https://api-security.owasp.org/?ref=arabroot.io)، وهدفها كما تقول [صفحة القائمة](https://api-security.owasp.org/editions/2023/en/0x11-t10/?ref=arabroot.io) ومقدمتها أن تعلم من يبني ال API ويصونه، من المطورين والمصممين والمعماريين والمديرين، أشهر نقاط الضعف في واجهات API الحديثة. وصدرت النسخة الأولى في 2019، والنسخة الحالية هي **OWASP API Security Top 10 2023**، وتصف نفسها في [ملاحظات الإصدار Release Notes](https://api-security.owasp.org/editions/2023/en/0x04-release-notes/?ref=arabroot.io) بأنها الإصدار الثاني بعد الأول بأربع سنوات بالضبط. أما سؤال النسخة الأحدث، فحتى أكتوبر 2026 لا يحتوي [مستودع المشروع على GitHub](https://github.com/OWASP/API-Security?ref=arabroot.io) إلا على مجلدي النسختين 2019 و2023، والتحديثات الأخيرة فيه في سبتمبر 2026 ترجمات لنسخة 2023، لذلك فالقائمة الحالية هي 2023، وإذا صدرت نسخة جديدة فسوف نحدث هذا المقال. والوثائق متاحة بترخيص Creative Commons Attribution-ShareAlike 4.0 كما يذكر موقع المشروع.

والمخاطر العشر بأسمائها الرسمية كما يلي:

| الرقم      | الاسم الرسمي                                       | بكلمات بسيطة                                                           |
| ---------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
| API1:2023  | Broken Object Level Authorization (BOLA)           | تقرأ أو تعدل سجلاً ليس لك بتغيير معرفه                                 |
| API2:2023  | Broken Authentication                              | المصادقة نفسها مكسورة: توكن لا يفحص، أو دخول بلا حد للمحاولات          |
| API3:2023  | Broken Object Property Level Authorization (BOPLA) | السجل لك، ولكن بعض حقوله ليست لك، لا قراءة ولا كتابة                   |
| API4:2023  | Unrestricted Resource Consumption                  | لا يوجد حد للحجم أو العدد أو الزمن أو التكلفة                          |
| API5:2023  | Broken Function Level Authorization (BFLA)         | مستخدم عادي يستدعي وظيفة المدير                                        |
| API6:2023  | Unrestricted Access to Sensitive Business Flows    | سكربت يستخدم مساراً تجارياً حساساً بكثرة تضر العمل، مثل حجز كل المقاعد |
| API7:2023  | Server Side Request Forgery (SSRF)                 | تجعل السيرفر يطلب عنواناً داخلياً لا تصل إليه أنت                      |
| API8:2023  | Security Misconfiguration                          | إعدادات خاطئة في أي طبقة من طبقات ال API                               |
| API9:2023  | Improper Inventory Management                      | واجهات ونسخ قديمة لا يعرف أحد أنها ما زالت تعمل                        |
| API10:2023 | Unsafe Consumption of APIs                         | الثقة العمياء بردود ال APIs الخارجية التي يستدعيها تطبيقك              |

### كيف بنيت نسخة 2023، ولماذا لا تجد فيها أرقاماً؟

هنا فرق مهم عن قائمة الويب، فكما شرحنا في [طريقة بناء OWASP Top 10:2025](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#how-built) تبنى قائمة الويب من بيانات اختبار ملايين التطبيقات، أما قائمة API فتقول صفحة [المنهجية والبيانات Methodology and Data](https://api-security.owasp.org/editions/2023/en/0xd0-about-data/?ref=arabroot.io) إن المشروع فتح باب جمع البيانات Call for Data من 1 سبتمبر إلى 30 نوفمبر 2022، ولم يصل منه ما يكفي لتحليل إحصائي له معنى. لذلك بنى الفريق القائمة من حوادث API منشورة ومن منصات مكافآت الثغرات Bug Bounty بين 2019 و2022، ثم من نقاشات مع مختصين، واستخدم منهجية OWASP لتقييم المخاطر Risk Rating Methodology، وحدد الانتشار Prevalence بالاتفاق بين أعضاء الفريق بحسب خبرتهم، ثم نشر نسخة مرشحة Release Candidate للنقاش العام قبل النسخة النهائية. وهذا يعني أن القائمة رأي خبراء مدروس وليست قياساً، وأن ترتيبها لا يعني أن الخطر الأول أكثر انتشاراً بنسبة معينة من الثاني، وتصف الصفحة نفسها القائمة بأنها وثيقة تنظر إلى الأمام Forward-looking للسنوات الثلاث أو الأربع التالية.

### ما الذي تغير عن نسخة 2019؟

بحسب [ملاحظات الإصدار](https://api-security.owasp.org/editions/2023/en/0x04-release-notes/?ref=arabroot.io) ومقارنة بقائمة 2019، أهم التغييرات ما يلي:

- دمج الخطرين «كشف البيانات الزائد Excessive Data Exposure» و«الإسناد الجماعي Mass Assignment» في خطر واحد هو API3 BOPLA، والسبب أن أصلهما واحد، وهو غياب التفويض على مستوى الحقل Property.
- الخطر الرابع كان اسمه «نقص الموارد وحد المعدل Lack of Resources and Rate Limiting» وأصبح «الاستهلاك غير المحدود للموارد Unrestricted Resource Consumption»، فالتركيز الآن على ما يستهلك، من ذاكرة ومعالج وتكلفة خدمات خارجية، وليس على سرعة الطلبات وحدها.
- ثلاثة مخاطر جديدة: API6 للمسارات التجارية الحساسة، وتقول ملاحظات الإصدار إنه يجمع أغلب التهديدات التي يخففها حد المعدل، وAPI7 SSRF، وAPI10 الاستهلاك غير الآمن لل APIs، والسبب كما تقول القائمة أن المهاجمين بدأوا يستهدفون الخدمات المتكاملة مع الهدف بدل الهدف نفسه.
- خرج من القائمة خطرا الحقن Injection والسجلات والمراقبة غير الكافية Insufficient Logging and Monitoring كبندين مستقلين، وهما ما زالا في قائمة الويب، فالحقن في [A05:2025](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#a05) والسجلات في [A09:2025](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#a09)، وكل ما هناك ينطبق على ال API تماماً.
- «إدارة الأصول Improper Assets Management» أصبحت «إدارة السجل Improper Inventory Management».

### القائمة وقوائم OWASP الأخرى

تقول [مقدمة القائمة](https://api-security.owasp.org/editions/2023/en/0x11-t10/?ref=arabroot.io) إن قوائم OWASP لا تحل إحداها محل الأخرى، فإذا كنت تبني تطبيق جوال أو تطبيق ويب يعمل فوق API فاقرأ القائمتين معاً، ولذلك فهذا المقال لا يعيد ما شرحناه في [OWASP Top 10 للمبتدئين](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وإنما يضيف ما يخص ال API. وإذا أردت الصورة الكاملة لمشاريع المؤسسة ووثائقها، فقد خصصنا لها مقال [ما هي OWASP؟ مشاريعها ووثائقها](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-owasp-%D9%85%D8%B4%D8%A7%D8%B1%D9%8A%D8%B9%D9%87%D8%A7-%D9%88%D9%88%D8%AB%D8%A7%D8%A6%D9%82%D9%87%D8%A7-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وإذا كان تطبيقك يمرر نصوص المستخدمين إلى نموذج لغة يستدعي أدوات وواجهات API، فالقائمة التي تكمل هذه هي [OWASP Top 10 لتطبيقات نماذج اللغة LLM](https://arabroot.io/articles/owasp-top-10-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D9%86%D9%85%D8%A7%D8%B0%D8%AC-%D8%A7%D9%84%D9%84%D8%BA%D8%A9-llm/).

والصورة التالية تبين المخاطر العشر مجمعة في خمسة أسئلة، وهذا التجميع من عندنا لتسهيل الفهم، ولاحظ أن ثلاثة من الخمسة الأولى أسئلة صلاحيات:

![خمسة أعمدة، الأول Authorization وفيه API1 BOLA وAPI3 BOPLA وAPI5 BFLA، والثاني Authentication وفيه API2 Broken Authentication، والثالث Limits and flows وفيه API4 Unrestricted Resource Consumption وAPI6 Unrestricted Access to Sensitive Business Flows، والرابع Outbound calls وفيه API7 Server Side Request Forgery وAPI10 Unsafe Consumption of APIs، والخامس Operations وفيه API8 Security Misconfiguration وAPI9 Improper Inventory Management](https://arabroot.io/content/images/2026/10/api-security-01-map.webp)

المخاطر العشر في OWASP API Security Top 10 لعام 2023 مجمعة حسب السؤال الذي يسأله كل خطر: ماذا يحق لك، ومن أنت، وكم مرة وبأي حجم، وماذا يطلب ال API من غيره، وما الذي يعمل في الإنتاج Production

## ما الذي يجعل ال API مختلفاً عن صفحة الويب؟

ال API في النهاية طلبات HTTP مثل أي صفحة، بنفس الطرق Methods ورموز الحالة Status Codes التي شرحناها في دليل [HTTP وHTTPS للمبتدئين](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#methods)، ولكن هناك أربعة فروق تجعل أخطاءه أسهل في الوقوع وأوسع في الأثر:

- **لا توجد واجهة تخفي شيئاً:** في تطبيق الويب التقليدي يبني السيرفر الصفحة، فإذا لم يضع الزر فلن يراه المستخدم، أما في ال API فالواجهة تعمل عند المستخدم، في المتصفح أو الجوال، وكل قرار تتخذه الواجهة يستطيع المستخدم أن يتجاوزه بإرسال الطلب بنفسه، وبالتالي فكل قاعدة لا يفرضها السيرفر غير موجودة.
- **المعرفات IDs ظاهرة في كل مكان:** الرابط `/orders/7002` والحقل `"customer_id": 51` في JSON، والمستخدم يراها ويستطيع أن يغيرها، ولذلك جاء BOLA في المرتبة الأولى.
- **الكلاينت Client آلة في الغالب:** تطبيق جوال، أو نظام شريك، أو سكربت، أو وكيل ذكاء اصطناعي Agent، وكلها ترسل آلاف الطلبات دون تعب، فما يبدو استخداماً عادياً لإنسان يصبح استنزافاً عندما يكرره برنامج.
- **الرد بيانات خام:** الصفحة تعرض ما اخترته من البيانات، أما ال API فيرد غالباً بالكائن كما هو بعد تحويله إلى JSON Serialization، فإذا كان في الكائن حقل حساس خرج معه.

وقد يتساءل البعض: إذا كان تطبيقنا الجوال هو الكلاينت الوحيد لهذا ال API، فمن سوف يرسل طلباً غيره؟ والإجابة أن ال API موجود على الإنترنت، وأن أي مستخدم يستطيع أن يضع على جهازه Proxy يقرأ طلبات التطبيق ويعدلها، فهو يرى الروابط والتوكن والحقول كلها، ويرسلها بأداة curl، ولذلك اعتبر أن كل Endpoint باب مفتوح لكل من يملك توكن صالحاً، وأن الواجهة مجرد زبون من الزبائن.

## ال API الذي سنمشي عليه في كل خطر

حتى لا تبقى المخاطر أسماء مجردة، سوف نأخذ API واحداً لمتجر صغير فيه ثلاثة مستخدمين: alice وbob وهما عميلان عاديان Role user، وcarol وهي مديرة Role admin، والنقاط Endpoints التالية:

| ال Endpoint                    | الوظيفة                            | الخطر الذي سوف نراه فيه    |
| ------------------------------ | ---------------------------------- | -------------------------- |
| GET /orders/{id}               | عرض طلب واحد                       | API1 BOLA                  |
| POST /auth/login والتوكن       | تسجيل الدخول والتحقق من التوكن     | API2 Broken Authentication |
| GET /users/me وPATCH /users/me | قراءة الملف الشخصي وتعديله         | API3 BOPLA                 |
| GET /orders?limit=N            | قائمة الطلبات                      | API4 استهلاك الموارد       |
| GET /admin/users               | تصدير كل المستخدمين للإدارة        | API5 BFLA                  |
| POST /bookings                 | حجز مقاعد في عرض محدود الكمية      | API6 المسارات التجارية     |
| POST /avatar/fetch             | جلب صورة الملف الشخصي من رابط      | API7 SSRF                  |
| ال API كله                     | الإعدادات والنسخ والخدمات الخارجية | API8 وAPI9 وAPI10          |

ولاحظ أن الأمثلة في كل قسم مختصرة عمداً لتبين الفكرة بالشكل الضعيف ثم المصلح، وأن خمسة من هذه المخاطر مكتوبة في [التجربة العملية](#lab) في ملف واحد يعمل بوضعين، فتراها تحدث فعلاً ثم تراها تختفي.

## API1:2023 تفويض مستوى الكائن المكسور Broken Object Level Authorization (BOLA)

### ما هو بلغة بسيطة؟

تقول [صفحة API1](https://api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/?ref=arabroot.io) إن كل Endpoint يستقبل معرف كائن Object ID ويقوم بأي عملية عليه يجب أن يفحص أن المستخدم الحالي يملك حق هذه العملية على هذا الكائن بعينه، وإن فشل هذا الفحص يؤدي إلى كشف البيانات أو تعديلها أو حذفها. وهو نفس ما سميناه IDOR في قائمة الويب، والفرق أن ال API مملوء بالمعرفات، في الرابط وفي الجسم Body وفي الترويسات Headers، فالفرصة أكبر.

ولاحظ نقطتين من الصفحة نفسها، الأولى أن مقارنة معرف المستخدم في التوكن بالمعرف الموجود في الطلب لا تكفي، لأنها تغطي حالات قليلة فقط، مثل `/users/{id}`، أما `/orders/7002` فلا يوجد فيه معرف مستخدم أصلاً، والسؤال الصحيح «هل الطلب 7002 مملوك لهذا المستخدم؟» وإجابته في قاعدة البيانات وليست في الرابط. والثانية أن الفرق بينه وبين BFLA أن المستخدم في BOLA يملك حق استدعاء ال Endpoint نفسه، والخلل في الكائن الذي يطلبه.

### الكود الضعيف والكود المصلح

المثال التالي بلغة Java مع Spring Boot وSpring Data JPA، والشكل الضعيف يتأكد من تسجيل الدخول ثم يثق بالمعرف:

```java
// Vulnerable: any logged-in user reads any order by changing the id
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable long id) {
    Order order = orders.findById(id).orElseThrow(NotFoundException::new);
    return OrderDto.from(order);
}
```

والشكل المصلح يضع الملكية في الاستعلام نفسه، فلا يمكن أن يخرج من قاعدة البيانات طلب لا يملكه المستخدم الحالي:

```java
// Fixed: the owner comes from the authenticated principal and is part of the query
public interface OrderRepository extends JpaRepository<Order, Long> {
    Optional<Order> findByIdAndOwnerId(long id, String ownerId);
}

@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable long id, @AuthenticationPrincipal Jwt jwt) {
    Order order = orders.findByIdAndOwnerId(id, jwt.getSubject())
            .orElseThrow(NotFoundException::new);   // same 404 for "not yours" and "not found"
    return OrderDto.from(order);
}
```

في الكود أعلاه لاحظ التالي:

- معرف المالك يؤخذ من التوكن الذي تحقق منه السيرفر، أي من الحقل `sub`، وليس من حقل في الطلب يستطيع المستخدم أن يغيره.
- الدالة `findByIdAndOwnerId` يولدها Spring Data من اسمها كما في [توثيق Query Methods](https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html?ref=arabroot.io)، فيصبح الشرط جزءاً من جملة SQL.
- الرد 404 في الحالتين، لأن الرد 403 للطلب الموجود و404 لغير الموجود يخبر المستخدم أي الأرقام موجودة.

### المنع Prevention والاكتشاف Detection

- **فحص في كل دالة تستخدم معرفاً من الكلاينت:** هذه صياغة OWASP نفسها، وأفضل طريقة لتطبيقها ألا تكتب الشرط يدوياً في كل مكان، وإنما في طبقة واحدة أو محرك صلاحيات، وقد شرحنا أين يكون هذا الفحص ولماذا لا تراه البوابة في قسم [أين يكون الفحص: البوابة أم التطبيق؟](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/#where-checks).
- **اجعل قاعدة البيانات تمنع الخطأ:** إذا كان ال API متعدد المستأجرين Multi-Tenant، فسياسات الصفوف Row-Level Security تجعل الاستعلام الذي نسي الشرط يعيد صفوف المستأجر الحالي فقط، كما في قسم [سياسات الصفوف Row-Level Security خطوة بخطوة](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/#rls).
- **المعرف العشوائي طبقة إضافية وليس فحصاً:** تنصح OWASP باستخدام معرفات عشوائية مثل GUID، وهذا يصعب التخمين، ولكن المعرف يتسرب في الروابط والسجلات والرسائل، لذلك اقرأ [الملاحظات الأمنية في مقال المعرفات الفريدة](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%B1%D9%81%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D8%AF%D8%A9-uuid-%D9%88-ulid-%D9%88-snowflake/#security) ولا تعتمد عليه وحده.
- **الاكتشاف بالاختبار:** تقول الصفحة اكتب اختبارات لآلية التفويض ولا تنشر تغييراً يفشلها، وأقوى اختبار هنا بسيط جداً، طلب بتوكن مستخدم آخر يتوقع الرفض، وهو أول اختبار في [التجربة](#lab-tests). أما أداة الفحص الديناميكي DAST فتحتاج حسابين وسيناريو مكتوباً حتى ترى هذا الخطر، كما في قسم [الفحص بعد تسجيل الدخول Authenticated Scan](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D8%AB%D9%86%D8%A7%D8%A1-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-dast-%D9%85%D8%B9-zap/#authenticated).
- **الاكتشاف في التشغيل:** سجل كل رفض بالمستخدم والكائن، فمستخدم يطلب مئة رقم متتالية في دقيقة يجب أن يطلق تنبيهاً، وقد شرحنا ما يدخل السجل في دليل [السجلات Logging للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#what-to-log).

## API2:2023 المصادقة المكسورة Broken Authentication

### ما هو بلغة بسيطة؟

المصادقة Authentication هي سؤال «من أنت؟»، وتعد [صفحة API2](https://api-security.owasp.org/editions/2023/en/0xa2-broken-authentication/?ref=arabroot.io) صوراً كثيرة لكسرها في ال API، منها: السماح بتجربة قوائم كلمات المرور المسربة Credential Stuffing دون حد، والسماح بتخمين كلمة مرور حساب واحد دون قفل أو CAPTCHA، وإرسال التوكن أو كلمة المرور في الرابط URL، وعدم التحقق من صحة التوكن، وقبول JWT غير موقع بالقيمة `{"alg":"none"}`، وعدم فحص تاريخ انتهاء JWT. وتضيف الصفحة أن الخدمة المصغرة Microservice معرضة أيضاً إذا كانت الخدمات الأخرى تصل إليها دون مصادقة، وأن نقطة «نسيت كلمة المرور» يجب أن تعامل معاملة نقطة الدخول تماماً.

### الكود الضعيف والكود المصلح

أشهر خطأ نراه في Node.js مع مكتبة [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken?ref=arabroot.io) أن يقرأ المطور التوكن بالدالة `decode` لأنها «تعمل»، والتوثيق نفسه يحذر من أنها لا تتحقق من صحة التوقيع:

```javascript
// Vulnerable: decode() reads the token but never checks the signature or the expiry
function auth(req, res, next) {
  const token = (req.headers.authorization || '').replace(/^Bearer /, '');
  req.user = jwt.decode(token);          // anyone can write {"sub":"carol","role":"admin"}
  if (!req.user) return res.status(401).end();
  next();
}
```

والشكل المصلح يتحقق من التوقيع ويحدد الخوارزمية المقبولة والجهة المصدرة والجمهور المقصود، ويرفض التوكن المنتهي:

```javascript
// Fixed: verify() checks signature, algorithm, issuer, audience and exp
function auth(req, res, next) {
  const token = (req.headers.authorization || '').replace(/^Bearer /, '');
  try {
    req.user = jwt.verify(token, PUBLIC_KEY, {
      algorithms: ['RS256'],             // never let the token choose its own algorithm
      issuer: 'https://id.example.com',
      audience: 'orders-api',
    });
    next();
  } catch {
    res.status(401).json({ error: 'invalid token' });
  }
}
```

وفي ASP.NET Core يأتي حد المعدل Rate Limiting مع إطار العمل منذ .NET 7، ونقطة الدخول تستحق حداً أشد من بقية ال API كما تقول OWASP:

```csharp
// Fixed: a strict limiter on the login endpoint only
builder.Services.AddRateLimiter(o =>
{
    o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;   // the default is 503
    o.AddFixedWindowLimiter("login", w => { w.PermitLimit = 5; w.Window = TimeSpan.FromMinutes(1); });
});
var app = builder.Build();
app.UseRateLimiter();
app.MapPost("/auth/login", Login).RequireRateLimiting("login");
```

في الكود أعلاه لاحظ التالي:

- القائمة `algorithms` تمنع هجمات تبديل الخوارزمية، ومنها `alg: none`، وقد شرحنا هذه الأخطاء في قسم [أخطاء تكررت مع JWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#jwt-pitfalls).
- الحقلان `issuer` و`audience` يمنعان أن يقبل ال API توكن صدر لخدمة أخرى من نفس مزود الهوية.
- الرمز الافتراضي للرفض في محدد المعدل في ASP.NET Core هو 503 بحسب [توثيق RejectionStatusCode](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptions.rejectionstatuscode?ref=arabroot.io)، والصحيح للكلاينت 429 Too Many Requests المعرف في [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585?ref=arabroot.io)، لأن 503 يقول له «السيرفر معطل» فيعيد المحاولة.

### المنع Prevention والاكتشاف Detection

- **لا تخترع المصادقة:** تقول OWASP لا تعد اختراع العجلة في المصادقة وتوليد التوكنات وتخزين كلمات المرور، واستخدم المعايير، وتذكر أن OAuth ليس مصادقة، وكذلك مفاتيح API Keys، وقد شرحنا الفرق في دليل [OAuth وOpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#delegation).
- **مفاتيح API Keys للتطبيقات وليست للمستخدمين:** المفتاح يعرف الكلاينت، أما المستخدم فيدخل بتدفق صحيح، والخدمة التي تتصل بخدمة تستخدم تدفق [Client Credentials](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#client-credentials).
- **حد أشد على الدخول واستعادة كلمة المرور:** ومعه قفل مؤقت للحساب Account Lockout، والمصادقة متعددة العوامل MFA حيث أمكن، وإعادة طلب كلمة المرور قبل العمليات الحساسة مثل تغيير البريد، وقد شرحنا الحد في ال Reverse Proxy في قسم [الحد من معدل الطلبات Rate Limiting](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%B9%D9%84%D9%89-%D9%85%D8%B3%D8%AA%D9%88%D9%89-http/#rate-limiting)، وتجد التفاصيل في [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication%5FCheat%5FSheet.html?ref=arabroot.io).
- **الاكتشاف:** اختبارات ترسل توكن منتهياً، وتوكن بتوقيع خاطئ، وتوكن لجمهور آخر، وتتوقع 401 في كل حالة، ثم سجل لكل فشل دخول مع تنبيه عند الارتفاع المفاجئ.

## API3:2023 تفويض مستوى خصائص الكائن المكسور Broken Object Property Level Authorization (BOPLA)

### ما هو بلغة بسيطة؟

قد يكون السجل لك فعلاً، ومع ذلك فبعض حقوله Properties ليست لك، وتقول [صفحة API3](https://api-security.owasp.org/editions/2023/en/0xa3-broken-object-property-level-authorization/?ref=arabroot.io) إن ال Endpoint معرض في حالتين: الأولى أن يكشف حقولاً حساسة لا يجب أن يقرأها المستخدم، وكان اسمها كشف البيانات الزائد Excessive Data Exposure، والثانية أن يسمح للمستخدم بتغيير حقل لا يحق له تغييره، وكان اسمها الإسناد الجماعي Mass Assignment. ومثال الأولى ملف شخصي يعيد هاش كلمة المرور Password Hash لأن الواجهة «لا تعرضه»، ومثال الثانية طلب PATCH يرسل فيه المستخدم الحقل `is_admin` فيقبله السيرفر لأن الكود ينسخ كل حقول الطلب إلى الكائن.

والصورة التالية تبين الجهتين، ما يخرج من الكائن في الرد وما يدخل إليه من الطلب:

![لوحتان، في اليسار reading excessive data exposure حيث صف المستخدم في قاعدة البيانات فيه id وdisplay_name وphone وrole وis_admin وpassword_hash وinternal_notes، ومسار أحمر return user يخرج كل الحقول، ومسار أخضر PublicUser DTO يخرج id وdisplay_name وphone فقط. وفي اليمين writing mass assignment حيث bob يرسل بأداة curl طلب PATCH فيه display_name وis_admin true، والمسار الأحمر Object.assign يجعل bob مديراً، والمسار الأخضر allowlist يقبل display_name وphone فقط ويرد 400 is_admin not allowed](https://arabroot.io/content/images/2026/10/api-security-03-bopla.webp)

الإصلاح في الجهتين قائمة صريحة بالحقول: نموذج للرد DTO يختار ما يخرج، ونموذج للطلب يقبل الحقول المسموحة ويرفض ما سواها

### الكود الضعيف والكود المصلح

المثال التالي بلغة C# مع ASP.NET Core Minimal APIs وEntity Framework Core، والشكل الضعيف يعيد الكيان Entity كما هو ويستقبله كما هو:

```csharp
// Vulnerable: the database entity is both the response and the request model
app.MapGet("/users/me", async (ClaimsPrincipal me, AppDb db) =>
    await db.Users.FindAsync(me.FindFirstValue("sub")));          // PasswordHash, IsAdmin... all leave

app.MapPatch("/users/me", async (User input, ClaimsPrincipal me, AppDb db) =>
{
    var user = await db.Users.FindAsync(me.FindFirstValue("sub"));
    db.Entry(user!).CurrentValues.SetValues(input);                // copies IsAdmin too
    await db.SaveChangesAsync();
    return Results.Ok(user);
});
```

والشكل المصلح يعرف نوعاً للرد ونوعاً للطلب، فلا يعرف أي منهما شيئاً عن الحقول الحساسة:

```csharp
// Fixed: one record for what leaves, one record for what may come in
public record PublicUser(string Id, string DisplayName, string Phone);
public record UpdateProfile(
    [property: StringLength(80)] string? DisplayName,
    [property: Phone] string? Phone);

app.MapGet("/users/me", async (ClaimsPrincipal me, AppDb db) =>
{
    var u = await db.Users.FindAsync(me.FindFirstValue("sub"));
    return new PublicUser(u!.Id, u.DisplayName, u.Phone);
});

app.MapPatch("/users/me", async (UpdateProfile input, ClaimsPrincipal me, AppDb db) =>
{
    var u = await db.Users.FindAsync(me.FindFirstValue("sub"));
    if (input.DisplayName is not null) u!.DisplayName = input.DisplayName;
    if (input.Phone is not null) u!.Phone = input.Phone;
    await db.SaveChangesAsync();
    return Results.Ok(new PublicUser(u!.Id, u.DisplayName, u.Phone));
});
```

وفي Python مع FastAPI تعطيك المكتبة الأداتين جاهزتين، فالمعامل `response_model` يرشح الرد إلى حقول النموذج كما في [توثيق Response Model](https://fastapi.tiangolo.com/tutorial/response-model/?ref=arabroot.io)، والإعداد `extra="forbid"` في Pydantic يرفض أي حقل غير معروف في الطلب كما في [توثيق Extra Data](https://docs.pydantic.dev/latest/concepts/models/?ref=arabroot.io#extra-data):

```python
# Fixed: unknown fields are a 422 error, and the response keeps only PublicUser fields
from pydantic import BaseModel, ConfigDict, Field

class UpdateProfile(BaseModel):
    model_config = ConfigDict(extra="forbid")
    display_name: str | None = Field(default=None, max_length=80)
    phone: str | None = Field(default=None, max_length=20)

class PublicUser(BaseModel):
    id: str
    display_name: str
    phone: str

@app.patch("/users/me", response_model=PublicUser)
def update_me(body: UpdateProfile, user: User = Depends(current_user)):
    for field, value in body.model_dump(exclude_unset=True).items():
        setattr(user, field, value)
    return user
```

في الكود أعلاه لاحظ التالي:

- لا تستخدم كيان قاعدة البيانات كنموذج للطلب أو للرد، والسبب أن أي عمود جديد يضاف إلى الجدول، مثل `internal_notes`، يظهر في ال API تلقائياً دون أن يقرر أحد ذلك.
- تقول OWASP تجنب الدوال العامة مثل `to_json()` واختر الحقول التي تريد إرجاعها بنفسك، وتجنب الربط التلقائي لمدخلات الكلاينت بالكائنات الداخلية، وتجد التفاصيل لكل إطار عمل في [OWASP Mass Assignment Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Mass%5FAssignment%5FCheat%5FSheet.html?ref=arabroot.io).
- الحقل غير المسموح يرفض برسالة واضحة، ولا يتجاهل بصمت، لأن التجاهل يخفي المحاولة عن السجل ويخفي الخطأ عن المطور الذي يستدعي ال API.

### المنع Prevention والاكتشاف Detection

- نموذج للرد ونموذج للطلب لكل عملية، وقائمة سماح Allowlist بالحقول القابلة للتعديل، وما سواها مرفوض.
- تحقق من مخطط الرد Response Schema كطبقة إضافية، وهذه توصية صريحة في الصفحة، وملف OpenAPI الذي نشرحه في [قسم OpenAPI](#openapi) هو المكان الطبيعي لهذا المخطط.
- الحقول التي تختلف بحسب الدور، مثل حقل الهامش الربحي الذي يراه المدير فقط، تحتاج نموذجين أو قاعدة تفويض على الحقل، وهذا ما يجعل GraphQL حساساً لهذا الخطر كما سيأتي في [قسم GraphQL](#graphql).
- **الاكتشاف:** اختبار يقرأ الرد ويتأكد من غياب الحقول الحساسة، واختبار يرسل حقلاً ممنوعاً ويتوقع الرفض، وكلاهما في [التجربة](#lab-tests). وأداة SAST قد تجد بعض أنماط الربط التلقائي، ولكنها لا تعرف أي الحقول حساسة في عملك، كما شرحنا في قسم [ما لا تراه أداة SAST](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/#blind-spots).

## API4:2023 الاستهلاك غير المحدود للموارد Unrestricted Resource Consumption

### ما هو بلغة بسيطة؟

كل طلب يستهلك شيئاً، من عرض الشبكة Bandwidth والمعالج CPU والذاكرة Memory والتخزين Storage، وأحياناً مالاً، فبعض الخدمات تدفع لها لكل طلب، مثل إرسال رسالة SMS أو بريد أو التحقق من الهوية. وتقول [صفحة API4](https://api-security.owasp.org/editions/2023/en/0xa4-unrestricted-resource-consumption/?ref=arabroot.io) إن ال API معرض إذا غاب أحد الحدود التالية أو كان غير مناسب: مهلة التنفيذ Execution Timeout، وأقصى ذاكرة، وعدد واصفات الملفات File Descriptors والعمليات Processes، وأقصى حجم لرفع الملف، وعدد العمليات في طلب واحد مثل تجميع الاستعلامات GraphQL Batching، وعدد السجلات في الصفحة الواحدة، وسقف الإنفاق Spending Limit لدى مزودي الخدمات الخارجية.

ولاحظ أن الخطر هنا ليس حجب الخدمة Denial of Service وحده، فطلب واحد بلا حد للصفحة يكفي ليخرج قاعدة البيانات كلها في رد واحد، ونقطة «أرسل لي رمز التحقق» بلا حد تتحول إلى فاتورة رسائل SMS على حسابك.

### الكود الضعيف والكود المصلح

المثال التالي بلغة Python مع FastAPI، والشكل الضعيف يأخذ حجم الصفحة من الكلاينت كما هو:

```python
# Vulnerable: the client decides how many rows leave the database
@app.get("/orders")
def list_orders(limit: int = 20, user: User = Depends(current_user)):
    return db.query(Order).limit(limit).all()
```

والشكل المصلح يضع حداً أدنى وأقصى للمعامل، ويرشح بالمالك، ويستخدم مؤشر الصفحة Cursor بدل رقم الصفحة:

```python
# Fixed: 1..100 rows, only the caller's orders, keyset pagination with a cursor
from fastapi import Query

@app.get("/orders")
def list_orders(limit: int = Query(20, ge=1, le=100),
                after_id: int | None = None,
                user: User = Depends(current_user)):
    q = db.query(Order).filter(Order.owner_id == user.id)
    if after_id is not None:
        q = q.filter(Order.id > after_id)
    rows = q.order_by(Order.id).limit(limit).all()
    return {"items": rows, "next": rows[-1].id if len(rows) == limit else None}
```

في الكود أعلاه لاحظ التالي:

- المعاملان `ge` و`le` يجعلان FastAPI يرفض القيمة خارج المدى قبل أن يصل الطلب إلى الدالة، كما في [توثيق Numeric Validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/?ref=arabroot.io)، ويظهر الحد نفسه في ملف OpenAPI الذي يولده.
- الحد هنا رفض صريح بالرمز 422 أو 400، وليس تقليصاً صامتاً إلى 100، والسبب أن الكلاينت الذي طلب ألف سجل يجب أن يعرف أنه لم يحصل عليها.
- الترقيم بالمؤشر Keyset Pagination أسرع من `OFFSET` في الجداول الكبيرة، لأن قاعدة البيانات لا تحتاج أن تقرأ كل الصفوف التي قبله.

والصورة التالية تبين أين يوضع كل حد، ولماذا لا يغني حد البوابة عن حد التطبيق:

![طلبات من الكلاينت ومن سكربت يطلب limit=100000 تمر بثلاث طبقات مرقمة: 1 gateway أو proxy فيها حد المعدل لكل مفتاح وعنوان وأقصى حجم للطلب ومهلة وحد أشد على login ورد 429 مع Retry-After، و2 application فيها أقصى حجم للصفحة 100 وطول النصوص وعدد العناصر وحصة لكل مستخدم ومهلة للطلبات الخارجية وعمق GraphQL وتكلفته، و3 database فيها LIMIT في كل استعلام قائمة ومهلة للاستعلام وحجم مجمع الاتصالات وفهرس للشرط، وتحتها 4 حدود ال Container للذاكرة والمعالج والعمليات، ومن التطبيق خط إلى خدمة SMS أو بريد مدفوعة لكل طلب عليها سقف إنفاق أو تنبيه فاتورة](https://arabroot.io/content/images/2026/10/api-security-04-limits.webp)

حد في كل طبقة: البوابة تحد الكثرة، والتطبيق يحد ما يعرف معناه، وقاعدة البيانات تحد زمن الاستعلام، وال Container يحد الذاكرة، والخدمة المدفوعة لها سقف إنفاق

### المنع Prevention والاكتشاف Detection

- **حدود ال Container:** تنصح OWASP بتشغيل ال API في بيئة تسهل تحديد الذاكرة والمعالج وعدد العمليات وعدد مرات إعادة التشغيل، مثل ال Containers.
- **حدود المدخلات:** أقصى طول للنص، وأقصى عدد لعناصر المصفوفة، وأقصى حجم للملف المرفوع، وقد شرحنا طبقات حجم الرفع في دليل [حدود حجم رفع الملفات](https://arabroot.io/articles/%D8%AD%D8%AF%D9%88%D8%AF-%D8%AD%D8%AC%D9%85-%D8%B1%D9%81%D8%B9-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA/).
- **حد المعدل لكل كلاينت ولكل عملية:** وتقول الصفحة إن بعض النقاط تحتاج سياسة أشد، مثل التحقق من رمز OTP أو طلب استعادة كلمة المرور.
- **سقف إنفاق Spending Limit** لكل خدمة مدفوعة، فإذا لم يتح المزود سقفاً فاضبط تنبيه الفاتورة Billing Alert على الأقل.
- **الاكتشاف:** اختبار يطلب صفحة أكبر من الحد ويتوقع الرفض، ثم اختبار ضغط Stress Test يرسل الطلبات الثقيلة بالتوازي ويراقب الذاكرة وزمن الرد، كما في دليل [اختبار الأحمال والضغط Load وStress Testing](https://arabroot.io/articles/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%AD%D9%85%D8%A7%D9%84-%D9%88%D8%A7%D9%84%D8%B6%D8%BA%D8%B7-load-%D9%88-stress-testing-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، ثم مقاييس Metrics لحجم الردود وزمنها لكل Endpoint في الإنتاج.

## API5:2023 تفويض مستوى الوظيفة المكسور Broken Function Level Authorization (BFLA)

### ما هو بلغة بسيطة؟

هنا المستخدم لا يغير معرف كائن، وإنما يصل إلى وظيفة Function لا يحق لدوره أصلاً، وتقترح [صفحة API5](https://api-security.owasp.org/editions/2023/en/0xa5-broken-function-level-authorization/?ref=arabroot.io) ثلاثة أسئلة لاكتشافه: هل يستطيع المستخدم العادي الوصول إلى نقاط الإدارة؟ وهل يستطيع أن ينفذ عملية حساسة بمجرد تغيير طريقة HTTP، مثل `GET` إلى `DELETE`؟ وهل يستطيع مستخدم من مجموعة أن يصل إلى وظيفة مجموعة أخرى بتخمين الرابط، مثل `/api/v1/users/export_all`؟ وتحذر الصفحة من أن تفترض أن ال Endpoint إداري أو عادي من مساره فقط، لأن نقاط الإدارة تجدها كثيراً بجانب النقاط العادية تحت `/api/users`.

والصورة التالية تبين الفرق بين هذا الخطر وBOLA، لأن الخلط بينهما يجعل الفريق يصلح أحدهما ويظن أنه أصلح الاثنين:

![لوحتان، في اليسار API1 object level BOLA حيث alice بدور user تستدعي GET /orders/{id} المسموح للمستخدمين، فيصل مسار أخضر إلى الطلب 7001 الذي تملكه alice ومسار أحمر إلى الطلب 7002 الذي يملكه bob، والسؤال هل هذا السجل بعينه لك. وفي اليمين API5 function level BFLA حيث alice بدور user وcarol بدور admin تستدعيان GET /admin/users المقفل للمديرين فقط، فترد على alice بالرمز 403 وعلى carol بالرمز 200، والسؤال هل هذه الوظيفة مسموحة لدورك أصلاً](https://arabroot.io/content/images/2026/10/api-security-02-bola-bfla.webp)

في BOLA يملك المستخدم حق الوظيفة ويطلب سجلاً ليس له، وفي BFLA يصل إلى وظيفة ليست لدوره، ويحتاج كل Endpoint الفحصين معاً

### الكود الضعيف والكود المصلح

المثال التالي بلغة Java مع Spring Security 6، والشكل الضعيف يكتفي بأن يكون المستخدم مسجلاً لكل المسارات:

```java
// Vulnerable: authenticated is the only rule, so /admin/** is open to every user
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
    return http.build();
}
```

والشكل المصلح يحدد الدور لكل مجموعة من الوظائف، ويضيف فحصاً على الدالة نفسها، فإذا نقل أحدهم الدالة إلى Controller آخر بقي الفحص معها:

```java
// Fixed: deny by default, admin functions need the ADMIN role, checked twice
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(a -> a
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/orders/**").hasRole("ADMIN")
            .requestMatchers("/orders/**", "/users/me").hasRole("USER")
            .anyRequest().denyAll())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
    return http.build();
}

@PreAuthorize("hasRole('ADMIN')")              // needs @EnableMethodSecurity
@GetMapping("/admin/users")
public List<AdminUserDto> exportUsers() { ... }
```

في الكود أعلاه لاحظ التالي:

- السطر الأخير `denyAll()` يعني أن أي مسار جديد ينساه المطور يرفض، وهذا هو الرفض الافتراضي Deny by Default الذي تطلبه الصفحة، والقواعد تقرأ بالترتيب كما في [توثيق Authorize HTTP Requests](https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html?ref=arabroot.io).
- القاعدة على `DELETE` تغلق الباب الذي يفتحه تغيير طريقة HTTP.
- الخاصية `@PreAuthorize` من [Method Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html?ref=arabroot.io) تبقي الفحص مع الدالة، والسبب أن قواعد المسارات تكسر بسهولة عند إعادة تسمية مسار.

### المنع Prevention والاكتشاف Detection

- وحدة تفويض واحدة متسقة تستدعيها كل الوظائف، والرفض هو الأصل، وكل وظيفة تعطى لدور بعينه، وتقول OWASP أن تجعل كل Controllers الإدارة ترث من Controller إداري مجرد يفحص الدور، وأن تفحص الدور داخل أي وظيفة إدارية موجودة في Controller عادي.
- إذا تعقدت الأدوار، فانتقل من الشروط المتفرقة إلى نموذج صلاحيات واضح، وقد قارنا RBAC وABAC وReBAC والمحركات المفتوحة في دليل [الصلاحيات المركزية Authorization](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/#rbac).
- البوابة تستطيع أن تمنع `/admin` عن غير المديرين كفحص عام، وهذا مفيد، ولكنه لا يغني عن الفحص في التطبيق لأن الوظيفة الإدارية قد لا تكون تحت `/admin` أصلاً.
- **الاكتشاف:** جدول بكل Endpoint وكل طريقة وكل دور والنتيجة المتوقعة، ثم اختبار آلي يمر على الجدول كله، وهذا أسهل مما يبدو لأن ملف OpenAPI يعطيك قائمة النقاط جاهزة.

## API6:2023 الوصول غير المحدود إلى المسارات التجارية الحساسة Unrestricted Access to Sensitive Business Flows

### ما هو بلغة بسيطة؟

هذا الخطر ليس خللاً في الكود بالمعنى المعتاد، فكل طلب فيه صحيح ومسموح، والمشكلة في الكثرة والآلية، وتضرب [صفحة API6](https://api-security.owasp.org/editions/2023/en/0xa6-unrestricted-access-to-sensitive-business-flows/?ref=arabroot.io) ثلاثة أمثلة: شراء كل مخزون منتج عليه طلب كبير لإعادة بيعه بسعر أعلى Scalping، وإنشاء التعليقات بسكربت Spam، وحجز كل المواعيد المتاحة فلا يجد المستخدمون الحقيقيون موعداً. وتنبه الصفحة إلى أن الخطر يختلف بين الأعمال، فإنشاء المنشورات بسكربت قد يكون إزعاجاً في شبكة اجتماعية ومرحباً به في أخرى، ولذلك يبدأ المنع من سؤال تجاري قبل أن يكون تقنياً.

### الكود الضعيف والكود المصلح

لنفرض أن المتجر يبيع مئة مقعد في عرض محدود، والمثال التالي بلغة JavaScript مع Express، والشكل الضعيف يقبل أي عدد من الحجوزات ما دام هناك مقاعد:

```javascript
// Vulnerable: a script with one account can take all 100 seats in a second
app.post('/bookings', auth, async (req, res) => {
  const { eventId, seats } = req.body;
  const booking = await db.reserve(eventId, req.user.id, seats);
  res.status(201).json(booking);
});
```

والشكل المصلح يضع القاعدة التجارية على السيرفر، ويحد المعدل لكل مستخدم وليس لكل عنوان IP فقط:

```javascript
// Fixed: the business rule lives on the server, and the limit is per user
import { rateLimit } from 'express-rate-limit';

const perUser = rateLimit({ windowMs: 60_000, limit: 5, keyGenerator: (req) => req.user.id });
const MAX_SEATS_PER_USER = 4;

app.post('/bookings', auth, perUser, async (req, res) => {
  const { eventId, seats } = req.body;
  if (!Number.isInteger(seats) || seats < 1 || seats > MAX_SEATS_PER_USER)
    return res.status(400).json({ error: `seats must be 1..${MAX_SEATS_PER_USER}` });
  const already = await db.seatsHeldBy(eventId, req.user.id);
  if (already + seats > MAX_SEATS_PER_USER)
    return res.status(409).json({ error: 'limit per customer reached' });
  const booking = await db.reserve(eventId, req.user.id, seats);
  log.info({ event: 'booking', user: req.user.id, eventId, seats });
  res.status(201).json(booking);
});
```

ولاحظ أن هذا الإصلاح يرفع التكلفة على السكربت ولا يمنعه تماماً، فمن يملك خمسين حساباً يحجز مئتي مقعد، وهذا ما تقوله الصفحة صراحة حين تصف أساليب المنع بأنها تبطئ التهديد الآلي Automated Threats، ولذلك فالحد لكل مستخدم بداية، وبعده تأتي الأساليب التي تذكرها OWASP: بصمة الجهاز Device Fingerprinting، واكتشاف الإنسان Human Detection بأداة CAPTCHA أو أنماط الكتابة، واكتشاف الأنماط غير البشرية، مثل مستخدم ينتقل من «أضف إلى السلة» إلى «أكمل الشراء» في أقل من ثانية، وحظر عناوين مخارج شبكة Tor والبروكسيات المعروفة إذا ناسب عملك. وكل ذلك يحتاج إلى أن تعرف أولاً أي مساراتك حساسة، وهذا سؤال لصاحب العمل قبل المطور.

### المنع Prevention والاكتشاف Detection

- **طبقتان كما تقول OWASP:** طبقة العمل Business تحدد المسارات التي تضر إذا استخدمت بكثرة، وطبقة الهندسة Engineering تختار الحماية المناسبة لكل مسار، وهذا مكانه الطبيعي جلسة [نمذجة التهديدات Threat Modeling](https://arabroot.io/articles/%D9%86%D9%85%D8%B0%D8%AC%D8%A9-%D8%A7%D9%84%D8%AA%D9%87%D8%AF%D9%8A%D8%AF%D8%A7%D8%AA-threat-modeling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).
- **واجهات الآلات أولاً:** تقول الصفحة إن واجهات المطورين والشركاء B2B هدف سهل لأنها غالباً لا تطبق كل وسائل الحماية، فلا تستثنها من الحدود لأن «العميل شركة».
- **الاكتشاف:** هذا الخطر لا تراه أداة فحص، وإنما يظهر في السجلات والمقاييس، مثل عدد الحجوزات لكل حساب، والزمن بين خطوات المسار، وتركز الحجوزات في عدد قليل من الحسابات الجديدة.

## API7:2023 طلب السيرفر المزور Server Side Request Forgery (SSRF)

### ما هو بلغة بسيطة؟

تقول [صفحة API7](https://api-security.owasp.org/editions/2023/en/0xa7-server-side-request-forgery/?ref=arabroot.io) إن SSRF يحدث عندما يجلب ال API مورداً من رابط يرسله المستخدم دون أن يتحقق منه، فيجعل المهاجم السيرفر يرسل طلباً إلى وجهة غير متوقعة، حتى لو كانت خلف جدار حماية Firewall أو VPN، لأن الطلب يخرج من داخل الشبكة. وتذكر الصفحة أن هذا الخطر أصبح أكثر انتشاراً لأن الميزات الحديثة تشجع عليه، مثل ال Webhooks وجلب الملفات من رابط وتسجيل الدخول الموحد المخصص ومعاينة الروابط URL Previews، وأكثر خطورة لأن السحابة وKubernetes وDocker تكشف قنوات إدارة عبر HTTP على مسارات معروفة. ومثالها الأشهر خدمة البيانات الوصفية Metadata Service في السحابة، التي يصل إليها أي برنامج داخل السيرفر وتعيد معلومات عنه، وقد تعيد مفاتيح مؤقتة، كما في [توثيق AWS لخدمة Instance Metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html?ref=arabroot.io). ويجدر الإشارة هنا إلى أن قائمة الويب لعام 2025 نقلت SSRF إلى داخل فئة التحكم في الوصول، كما ذكرنا في [ما الذي تغير في نسخة 2025](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#whats-new)، أما قائمة API فتبقيه خطراً مستقلاً.

والصورة التالية تبين ميزة جلب الصورة في متجرنا قبل الإصلاح وبعده، وهي نفسها ما سوف نراه في التجربة:

![داخل شبكة داخلية اسمها lab-api-net ترسل alice طلب POST /avatar/fetch برابط http://meta:8080 إلى orders API الذي يمر الرابط فيه بخمس خطوات مرقمة: 1 قراءة الرابط بالصنف URL، 2 المخطط والمضيف والمنفذ في قائمة السماح، 3 إعادة التوجيه لا تتبع، 4 نوع المحتوى image والحجم والمهلة، 5 لا يعاد الرد الخام. المسار الأخضر يصل إلى avatars:8080 المضيف الوحيد المسموح ويرد stored 66 bytes، والمسار الأحمر المقطوع إلى meta:8080 الخدمة التي تقلد البيانات الوصفية وتعيد مفاتيح يرد 400 not allowed، وخط برتقالي من avatars إلى meta عبر go?to=meta بالرمز 302 لا يتبع](https://arabroot.io/content/images/2026/10/api-security-05-ssrf.webp)

قائمة سماح صريحة بالمضيف والمخطط والمنفذ، ودون تتبع لإعادة التوجيه، وبفحص نوع المحتوى، ودون إعادة الرد الخام إلى المستخدم

### الكود الضعيف والكود المصلح

المثال التالي بلغة Python مع مكتبة requests، والشكل الضعيف يطلب أي رابط ويعيد الرد كما هو:

```python
# Vulnerable: any scheme, any host, redirects followed, raw body returned
@app.post("/avatar/fetch")
def fetch_avatar(body: AvatarUrl, user: User = Depends(current_user)):
    r = requests.get(body.url)
    return {"status": r.status_code, "body": r.text}
```

والشكل المصلح يقرأ الرابط بمحلل URL موثوق، ويقبل المضيفين المسموحين فقط، ويتأكد أن العنوان الذي يحل إليه الاسم ليس داخلياً، ولا يتبع إعادة التوجيه، ويحد الحجم والمهلة ونوع المحتوى:

```python
# Fixed: allowlist of hosts, resolved address checked, no redirects, size and type limits
import ipaddress, socket
from urllib.parse import urlsplit

ALLOWED_HOSTS = {"avatars.example-cdn.com"}

def is_public(host: str) -> bool:
    for info in socket.getaddrinfo(host, 443):
        ip = ipaddress.ip_address(info[4][0])
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            return False
    return True

@app.post("/avatar/fetch")
def fetch_avatar(body: AvatarUrl, user: User = Depends(current_user)):
    u = urlsplit(body.url)
    if u.scheme != "https" or u.hostname not in ALLOWED_HOSTS or u.port not in (None, 443) or not is_public(u.hostname):
        raise HTTPException(400, "host not allowed")
    r = requests.get(body.url, allow_redirects=False, timeout=3, stream=True)
    if r.status_code != 200 or not r.headers.get("content-type", "").startswith("image/"):
        raise HTTPException(400, "not an image")
    data = r.raw.read(1_000_001)
    if len(data) > 1_000_000:
        raise HTTPException(400, "image too large")
    save_avatar(user.id, data)
    return {"stored": True, "bytes": len(data)}       # never the raw response
```

في الكود أعلاه لاحظ التالي:

- قائمة السماح Allowlist أقوى من قائمة المنع Blocklist، والسبب أن العناوين الداخلية تكتب بأشكال كثيرة، بالأرقام العشرية والست عشرية وIPv6 والأسماء التي تحل إلى عناوين داخلية، وقائمة المنع تنسى واحداً منها دائماً.
- فحص العنوان بعد حل الاسم DNS Resolution مهم لأن اسماً مسموحاً قد يحل إلى عنوان داخلي، ويبقى بين الفحص والطلب فاصل زمني قد يتغير فيه الحل DNS Rebinding، لذلك يقترح [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server%5FSide%5FRequest%5FForgery%5FPrevention%5FCheat%5FSheet.html?ref=arabroot.io) طبقة شبكة أيضاً تمنع خروج الطلبات إلى العناوين الداخلية.
- `allow_redirects=False` لأن المضيف المسموح قد يعيد التوجيه إلى عنوان داخلي، و`timeout` لأن مكتبة requests بلا مهلة قد تنتظر إلى الأبد كما يحذر [توثيقها](https://requests.readthedocs.io/en/latest/user/quickstart/?ref=arabroot.io#redirection-and-history).

### المنع Prevention والاكتشاف Detection

- تقول OWASP اعزل آلية جلب الموارد في الشبكة، لأنها مصممة لجلب موارد خارجية وليست داخلية، واستخدم قوائم سماح للمصادر وللمخططات والمنافذ ولأنواع المحتوى، وعطل إعادة التوجيه، واستخدم محلل URL مختبراً، ولا ترسل الردود الخام إلى الكلاينت.
- في السحابة فعل النسخة الأحدث من خدمة البيانات الوصفية التي تتطلب توكن جلسة، مثل IMDSv2 في AWS، كطبقة إضافية.
- **الاكتشاف:** اختبار يرسل عنواناً داخلياً ويتوقع الرفض، وهو في [التجربة](#lab-tests)، وسجل لكل رابط مرفوض، ومراقبة الطلبات الخارجة من السيرفر Egress، لأن سيرفر ال API الذي يطلب فجأة عناوين داخلية لم يطلبها من قبل علامة واضحة.

## API8:2023 أخطاء الإعداد الأمني Security Misconfiguration

### ما هو بلغة بسيطة؟

نفس فكرة A02 في قائمة الويب، ولكن على كل طبقات ال API، وتعد [صفحة API8](https://api-security.owasp.org/editions/2023/en/0xa8-security-misconfiguration/?ref=arabroot.io) منها: غياب التقوية Hardening أو صلاحيات خاطئة على الخدمات السحابية، وتحديثات أمنية ناقصة، وميزات غير لازمة مفعلة مثل طرق HTTP لا يحتاجها أحد، واختلاف طريقة معالجة الطلب بين سيرفرات السلسلة مثل ال Load Balancer وال Reverse Proxy والتطبيق، وغياب TLS، وغياب تعليمات التخزين المؤقت Cache Control، وسياسة CORS مفقودة أو خاطئة، ورسائل خطأ فيها Stack Trace.

### الكود الضعيف والكود المصلح

المثال التالي بلغة C# مع ASP.NET Core، والشكل الضعيف ينشر الإنتاج ببيئة التطوير ويقبل CORS من أي مصدر مع بيانات الدخول:

```csharp
// Vulnerable: Development environment in production (detailed errors),
// and CORS that reflects any origin with credentials
// docker run -e ASPNETCORE_ENVIRONMENT=Development ...
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p
    .SetIsOriginAllowed(_ => true).AllowCredentials().AllowAnyHeader().AllowAnyMethod()));
```

والشكل المصلح يعيد خطأ موحداً بصيغة Problem Details دون تفاصيل داخلية، ويسمح بمصدر الواجهة فقط وبالطرق التي يحتاجها:

```csharp
// Fixed: generic RFC 9457 errors, and an explicit CORS allowlist
builder.Services.AddProblemDetails();
builder.Services.AddCors(o => o.AddPolicy("web", p => p
    .WithOrigins("https://shop.example.com")
    .WithMethods("GET", "POST", "PATCH")
    .WithHeaders("Authorization", "Content-Type")));

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();      // no stack traces to the client
    app.UseHsts();
}
app.UseCors("web");
```

في الكود أعلاه لاحظ التالي:

- في ASP.NET Core تظهر صفحة الخطأ التفصيلية تلقائياً في بيئة Development كما في [توثيق Error Handling](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?ref=arabroot.io)، لذلك فالمتغير `ASPNETCORE_ENVIRONMENT` في الإنتاج إعداد أمني وليس تفصيلاً، وتجد إعداد البيئات في [توثيق Environments](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?ref=arabroot.io).
- الصيغة [Problem Details في RFC 9457](https://www.rfc-editor.org/rfc/rfc9457?ref=arabroot.io) تعطي الكلاينت رداً منظماً عن الخطأ، وتجعل المخطط ثابتاً حتى في حالة الخطأ، وهذه توصية صريحة في صفحة API8.
- إعادة أي Origin كما وصل مع `AllowCredentials` هي أخطر أخطاء CORS، وقد شرحنا السبب بالتجربة في قسم [إعادة أي Origin كما وصل](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-cors-%D9%88%D8%B3%D9%8A%D8%A7%D8%B3%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D8%A7%D9%84%D9%88%D8%A7%D8%AD%D8%AF/#reflect-origin)، وتذكر أن [CORS ليست مصادقة ولا صلاحيات](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-cors-%D9%88%D8%B3%D9%8A%D8%A7%D8%B3%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D8%A7%D9%84%D9%88%D8%A7%D8%AD%D8%AF/#not-auth)، فهي تحمي المتصفح فقط ولا تمنع curl.

### المنع Prevention والاكتشاف Detection

- تقوية قابلة للتكرار Repeatable Hardening ومراجعة دورية للإعدادات في كل الطبقات، من ملفات التشغيل إلى صلاحيات التخزين السحابي، وفحص آلي مستمر للإعدادات في كل البيئات.
- TLS في كل اتصال، بين الكلاينت وال API وبين ال API والخدمات الداخلية أيضاً، وطرق HTTP محددة لكل Endpoint وما سواها معطل.
- ترويسات الأمان للواجهات التي يستهلكها المتصفح، كما في دليل [ترويسات الأمان HSTS وCSP](https://arabroot.io/articles/%D8%AA%D8%B1%D9%88%D9%8A%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%85%D8%A7%D9%86-hsts-%D9%88-csp/)، وتقليل ما يكشفه السيرفر عن نفسه كما في قسم [لا تعط المهاجم معلومات مجانية](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%B9%D9%84%D9%89-%D9%85%D8%B3%D8%AA%D9%88%D9%89-http/#hide-info).
- أنواع المحتوى المقبولة محددة، فال API الذي يتوقع JSON يرفض XML بدل أن يمرره إلى محلل لا يعرف أحد أنه مفعل.
- **الاكتشاف:** هذا الخطر تجيده الأدوات الآلية، فالفحص الأساسي Baseline في [DAST مع ZAP](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D8%AB%D9%86%D8%A7%D8%A1-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-dast-%D9%85%D8%B9-zap/) يجد الترويسات الناقصة ورسائل الخطأ التفصيلية وإعلان الإصدار، وأدوات فحص الإعدادات تجد الباقي.

## API9:2023 إدارة السجل غير الصحيحة Improper Inventory Management

### ما هو بلغة بسيطة؟

لا تستطيع أن تحمي ما لا تعرف أنه موجود، وتقول [صفحة API9](https://api-security.owasp.org/editions/2023/en/0xa9-improper-inventory-management/?ref=arabroot.io) إن ال APIs تكشف نقاطاً أكثر من تطبيقات الويب التقليدية، وإن تشغيل أكثر من نسخة من ال API يوسع مساحة الهجوم. وتسمي الصفحة نوعين من النقاط العمياء: النقطة العمياء في التوثيق Documentation Blindspot، عندما لا يعرف أحد على وجه اليقين أي بيئة يعمل فيها ال Host، إنتاج أم اختبار أم تطوير، ومن يحق له الوصول إليه، وأي نسخة تعمل عليه، أو عندما لا يوجد توثيق أو خطة إيقاف لكل نسخة. والنقطة العمياء في تدفق البيانات Data Flow Blindspot، عندما يشارك ال API بيانات حساسة مع طرف ثالث دون مبرر عمل أو موافقة أو سجل لهذا التدفق.

والمثال المعتاد نسخة `v1` قديمة على نطاق فرعي نسيه الجميع، ما زالت تقرأ من قاعدة بيانات الإنتاج، وفيها الثغرة التي أصلحت في `v2` قبل سنة، أو بيئة اختبار علنية فيها نسخة من بيانات العملاء. والصورة التالية تبين دورة حياة نسخة ال API حول سجل واحد:

![حلقة من ست خطوات مرقمة حول مربع في الوسط اسمه inventory + openapi.yaml فيه host وenvironment وversion وowner وaudience public أو partners وdata and third parties، والخطوات: 1 design بالعقد أولاً مع المراجعة، 2 build in CI مع lint والتوثيق واختبارات العقد، 3 deploy ومسارات البوابة منه، 4 run مع السجلات وDAST من الملف، 5 deprecate مع ترويستي Deprecation وSunset، 6 retire بحذف المسار وDNS وال Host. وفي اليمين لوحة outside the inventory فيها سيرفر old-api.example.com/v1 بلا مالك ولا تحديثات ما زال يقرأ بيانات الإنتاج، وحشرة، وطرق البحث عنه: DNS وسجلات البوابة وحسابات السحابة والشهادات](https://arabroot.io/content/images/2026/10/api-security-06-inventory.webp)

كل نسخة تدخل السجل عند التصميم وتخرج منه عند الحذف الفعلي، وما يعمل في الإنتاج وليس في السجل هو أول ما تبحث عنه

### السجل في الملف: OpenAPI والإيقاف Deprecation

هذا الخطر لا يصلح بسطر كود، وإنما بعادة، وأبسط صورة لها أن يكون لكل API ملف [OpenAPI](#openapi) محفوظ في Git مع الكود، يقول أين يعمل ومن يملكه ومن جمهوره، ويعلم العمليات القديمة بأنها موقوفة. والمثال التالي جزء من هذا الملف:

```yaml
openapi: 3.2.0
info:
  title: Orders API
  version: 2.4.0
  contact: { name: orders-team, email: orders-team@example.com }
  x-audience: public            # public | partners | internal (our own convention)
servers:
  - url: https://api.example.com/v2
    description: production
paths:
  /v1/orders/{id}:
    get:
      deprecated: true          # still served until the Sunset date, then removed
      description: Use /v2/orders/{id}. Removed on 2027-03-31.
```

وحتى يعرف الكلاينت الذي ما زال يستدعي النسخة القديمة، يرسل السيرفر معها ترويستين معياريتين، الأولى `Deprecation` من [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745?ref=arabroot.io) وقيمتها تاريخ الإيقاف بصيغة الثواني منذ 1970، وهي في المثال 1 أكتوبر 2026، والثانية `Sunset` من [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594?ref=arabroot.io) وتقول متى يتوقف المورد عن الرد:

```text
HTTP/1.1 200 OK
Deprecation: @1790812800
Sunset: Wed, 31 Mar 2027 00:00:00 GMT
Link: <https://api.example.com/v2/orders/7001>; rel="successor-version"
```

📌

في 5 يناير 2023 اكتشفت شركة الاتصالات الأمريكية T-Mobile أن جهة ما تسحب بيانات من خلال API واحد دون تفويض، وذكرت في [إفصاحها الرسمي لهيئة الأوراق المالية SEC](https://www.sec.gov/Archives/edgar/data/1283699/000119312523010949/d641142d8k.htm?ref=arabroot.io) أن السحب بدأ في حوالي 25 نوفمبر 2022، وأنه شمل بيانات حوالي 37 مليون حساب، من الاسم وعنوان الفاتورة والبريد ورقم الجوال وتاريخ الميلاد ورقم الحساب، وأن ال API لم يكن يتيح أرقام البطاقات أو كلمات المرور. ولم يذكر الإفصاح الخلل التقني بالتفصيل، ولكن الدرس الذي يهمنا واضح من التواريخ وحدها، فسحب ملايين السجلات من API واحد استمر قرابة ستة أسابيع قبل أن يلاحظه أحد، ولذلك فكل API يجب أن يكون في السجل بمالك معروف، وأن يراقب حجم ما يخرج منه لكل كلاينت، فيطلق تنبيهاً عندما يخرج منه في يوم ما لم يخرج في شهر.

### المنع Prevention والاكتشاف Detection

- سجل لكل Host فيه البيئة والجمهور والنسخة، وسجل للخدمات المتكاملة فيه دور كل خدمة والبيانات التي تتبادلها وحساسيتها، وهذا نص توصيات الصفحة.
- توثيق مولد آلياً من معيار مفتوح ضمن خط CI/CD، ومتاح لمن يحق له استخدام ال API فقط.
- الحماية الخارجية، مثل البوابة وحد المعدل، على كل النسخ المكشوفة وليس على النسخة الحالية فقط، ولا بيانات إنتاج في بيئات غير الإنتاج، فإذا اضطررت فلها نفس حماية الإنتاج.
- عندما تحمل النسخة الجديدة إصلاحاً أمنياً، فقرر صراحة: هل تنقل الإصلاح إلى النسخة القديمة، أم توقفها بسرعة وتجبر الكلاينت على الانتقال؟
- **الاكتشاف:** قارن ما تراه في سجلات البوابة وسجلات DNS والشهادات الصادرة لنطاقاتك وحسابات السحابة بما في السجل، فكل مسار يستقبل طلبات وليس في الملف هو API ظل Shadow API يجب أن تجد مالكه أو تحذفه.

## API10:2023 الاستهلاك غير الآمن لل APIs Unsafe Consumption of APIs

### ما هو بلغة بسيطة؟

تطبيقك ليس مزوداً لل API فقط، وإنما مستهلك لواجهات غيره، من بوابة الدفع إلى خدمة الخرائط ومزود الرسائل، وتقول [صفحة API10](https://api-security.owasp.org/editions/2023/en/0xaa-unsafe-consumption-of-apis/?ref=arabroot.io) إن المطورين يثقون بالبيانات القادمة من APIs الطرف الثالث أكثر من مدخلات المستخدم، خاصة إذا كانت من شركات معروفة، فيخففون معايير التحقق Validation والتنظيف Sanitization. وال API معرض إذا تواصل مع APIs أخرى دون تشفير، أو لم يتحقق من بياناتها قبل معالجتها أو تمريرها، أو اتبع إعادة التوجيه دون تفكير، أو لم يحد الموارد التي يخصصها لمعالجة ردودها، أو لم يضع مهلة Timeout للتعامل معها.

### الكود الضعيف والكود المصلح

لنفرض أن المتجر يرسل العنوان إلى خدمة خارجية لتصحيحه، ثم يحفظ ما يعود منها، والمثال التالي بلغة Java مع `java.net.http.HttpClient`، والشكل الضعيف يتبع أي إعادة توجيه ولا يضع مهلة ويحفظ الرد كما هو:

```java
// Vulnerable: follows any redirect, no timeout, trusts every field it gets back
HttpClient client = HttpClient.newBuilder()
        .followRedirects(HttpClient.Redirect.ALWAYS)
        .build();
HttpResponse<String> r = client.send(
        HttpRequest.newBuilder(URI.create(GEO_API + "?q=" + query)).build(),
        HttpResponse.BodyHandlers.ofString());
Address a = mapper.readValue(r.body(), Address.class);
jdbc.update("UPDATE orders SET address = '" + a.line() + "' WHERE id = " + orderId);
```

والشكل المصلح يعامل الرد كأنه مدخل من مستخدم مجهول، فيحد الزمن والحجم، ولا يتبع إعادة التوجيه، ويتحقق من الحقول، ويستخدم استعلاماً بالمعاملات:

```java
// Fixed: TLS endpoint, timeouts, no redirects, size cap, validation, parameterized SQL
HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .followRedirects(HttpClient.Redirect.NEVER)     // also the default
        .build();
HttpRequest req = HttpRequest.newBuilder(URI.create(GEO_API + "?q=" + URLEncoder.encode(query, UTF_8)))
        .timeout(Duration.ofSeconds(5)).build();
HttpResponse<String> r = client.send(req, HttpResponse.BodyHandlers.ofString());
if (r.statusCode() != 200 || r.body().length() > 20_000) throw new UpstreamException();

Address a = mapper.readValue(r.body(), Address.class);
if (a.line() == null || a.line().length() > 200 || !a.country().equals("SA")) throw new UpstreamException();
jdbc.update("UPDATE orders SET address = ? WHERE id = ?", a.line(), orderId);
```

في الكود أعلاه لاحظ التالي:

- السياسة الافتراضية لإعادة التوجيه في `HttpClient` هي `NEVER` كما في [توثيق HttpClient.Builder](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.Builder.html?ref=arabroot.io)، أي أن الخطأ هنا قرار كتبه المطور، وتقول OWASP احتفظ بقائمة سماح للمواقع التي يحق لل API الخارجي أن يحولك إليها.
- الحقن عبر بيانات الطرف الثالث حقن عادي، والعلاج نفسه، أي الاستعلام بالمعاملات Parameterized Query كما في [A05 في قائمة الويب](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#a05).
- الرد الذي فشل في التحقق يرفض ويسجل، ولا يصلح تلقائياً، لأن الخدمة الخارجية التي بدأت ترسل بيانات غريبة قد تكون اخترقت.

### المنع Prevention والاكتشاف Detection

- عند اختيار مزود خدمة، قيم مستوى أمن ال API الخاص به، واجعل كل التواصل عبر TLS، وتحقق من كل البيانات القادمة منه ونظفها قبل استخدامها.
- ضع مهلة وحداً للحجم لكل اتصال خارجي، فالخدمة البطيئة تستهلك اتصالاتك وخيوطك Threads حتى يتوقف تطبيقك معها.
- وال Webhooks التي تصلك من مزود خارجي هي أيضاً استهلاك لواجهته، وسوف نرى كيف تتحقق منها في [قسم ال Webhooks](#webhooks).
- **الاكتشاف:** مراجعة الكود لكل عميل HTTP في المشروع، وأداة SAST تجد الاستعلامات المبنية بدمج النصوص، والسجلات تكشف الردود المرفوضة والمهلات المنتهية لكل خدمة خارجية.

## تجربة عملية Lab: API طلبات فيه خمس ثغرات، ثم إصلاحها

الآن سوف نرى خمسة من المخاطر تحدث فعلاً، ثم نراها تختفي. كتبنا API صغيراً بمكتبة Node.js القياسية وحدها، دون أي مكتبة خارجية، فيه 100000 طلب وثلاثة مستخدمين، ويعمل بوضعين يحددهما المتغير `MODE`: الوضع الضعيف `vuln` والوضع المصلح `fixed`، ومعه خدمتان داخليتان: خدمة `avatars` وهي المضيف الوحيد المسموح لجلب الصور، وخدمة `meta` التي تقلد خدمة البيانات الوصفية في السحابة وتعيد مفاتيح وهمية لا قيمة لها. وكل شيء يعمل على شبكة Docker خاصة، وال API وحده منشور على `127.0.0.1`.

### ملفات التجربة

الملف `app.mjs` كاملاً كما يلي، ولاحظ أن كل خطر في قسم واحد، والفرق بين الوضعين ظاهر في كل قسم:

```javascript
// Orders API for the lab: MODE=vuln keeps five API Top 10 flaws, MODE=fixed closes them.
import http from 'node:http';

const MODE = process.env.MODE || 'vuln';                       // vuln | fixed

// Three users and their tokens (in a real app: a validated JWT or a server-side session)
const TOKENS = { 'tok-alice': 'alice', 'tok-bob': 'bob', 'tok-carol': 'carol' };
const USERS = {
  alice: { id: 'alice', display_name: 'Alice', phone: '0500000001', role: 'user',
           is_admin: false, password_hash: '$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA' },
  bob:   { id: 'bob', display_name: 'Bob', phone: '0500000002', role: 'user',
           is_admin: false, password_hash: '$argon2id$v=19$m=19456,t=2,p=1$c2FsdDI$aGFzaDI' },
  carol: { id: 'carol', display_name: 'Carol', phone: '0500000003', role: 'admin',
           is_admin: true, password_hash: '$argon2id$v=19$m=19456,t=2,p=1$c2FsdDM$aGFzaDM' },
};

// 100000 orders: 7001 is alice's, 7002 is bob's, the rest belong to other customers
const ORDERS = new Map();
for (let i = 1; i <= 100000; i++) {
  const id = 7000 + i;
  const owner = id === 7001 ? 'alice' : id === 7002 ? 'bob' : `customer${i % 500}`;
  ORDERS.set(id, { id, owner, item: `item-${i % 97}`, total: (i * 37) % 9000 + 10,
                   address: `${i} King Fahd Rd, Riyadh` });
}

const ALLOWED_AVATAR_HOSTS = new Set(['avatars:8080']);       // fixed mode only
const MAX_PAGE = 100;
const EDITABLE = new Set(['display_name', 'phone']);           // fixed mode only

const send = (res, status, body) => {
  const data = JSON.stringify(body);
  res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) });
  res.end(data);
};
const audit = (event, fields) => console.error(JSON.stringify({ event, ...fields }));
const publicUser = (u) => ({ id: u.id, display_name: u.display_name, phone: u.phone });   // the response DTO

const readJson = (req, limit = 4096) => new Promise((ok, fail) => {
  let size = 0; const parts = [];
  req.on('data', (c) => { size += c.length; if (size > limit) { fail(new Error('too large')); req.destroy(); } else parts.push(c); });
  req.on('end', () => { try { ok(JSON.parse(Buffer.concat(parts).toString() || '{}')); } catch { fail(new Error('bad json')); } });
});

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://lab');
  const user = USERS[TOKENS[(req.headers.authorization || '').replace(/^Bearer /, '')]];
  if (!user) return send(res, 401, { error: 'login required' });

  // API1 BOLA: GET /orders/{id}
  const m = url.pathname.match(/^\/orders\/(\d+)$/);
  if (m && req.method === 'GET') {
    const order = ORDERS.get(Number(m[1]));
    if (MODE === 'fixed' && (!order || order.owner !== user.id)) {
      audit('access_denied', { user: user.id, object: `order:${m[1]}` });
      return send(res, 404, { error: 'not found' });
    }
    return order ? send(res, 200, order) : send(res, 404, { error: 'not found' });
  }

  // API4 Unrestricted Resource Consumption: GET /orders?limit=N
  if (url.pathname === '/orders' && req.method === 'GET') {
    let limit = Number(url.searchParams.get('limit') || 20);
    let rows = [...ORDERS.values()];
    if (MODE === 'fixed') {
      if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE)
        return send(res, 400, { error: `limit must be between 1 and ${MAX_PAGE}` });
      rows = rows.filter((o) => o.owner === user.id);          // and only the caller's orders
    }
    return send(res, 200, { count: Math.min(limit, rows.length), items: rows.slice(0, limit) });
  }

  // API3 BOPLA: GET and PATCH /users/me
  if (url.pathname === '/users/me' && req.method === 'GET')
    return send(res, 200, MODE === 'fixed' ? publicUser(user) : user);
  if (url.pathname === '/users/me' && req.method === 'PATCH') {
    let body;
    try { body = await readJson(req); } catch (e) { return send(res, 400, { error: e.message }); }
    if (MODE === 'fixed') {
      const unknown = Object.keys(body).filter((k) => !EDITABLE.has(k));
      if (unknown.length) {
        audit('rejected_fields', { user: user.id, fields: unknown });
        return send(res, 400, { error: `fields not allowed: ${unknown.join(', ')}` });
      }
      for (const k of EDITABLE) if (typeof body[k] === 'string') user[k] = body[k].slice(0, 80);
      return send(res, 200, publicUser(user));
    }
    Object.assign(user, body);                                   // binds every field the client sends
    if (body.is_admin === true) user.role = 'admin';
    return send(res, 200, user);
  }

  // API5 BFLA: GET /admin/users
  if (url.pathname === '/admin/users' && req.method === 'GET') {
    if (MODE === 'fixed' && user.role !== 'admin') {
      audit('function_denied', { user: user.id, function: 'GET /admin/users' });
      return send(res, 403, { error: 'forbidden' });
    }
    return send(res, 200, Object.values(USERS).map((u) => (MODE === 'fixed' ? { ...publicUser(u), role: u.role } : u)));
  }

  // API7 SSRF: POST /avatar/fetch {"url": "..."}
  if (url.pathname === '/avatar/fetch' && req.method === 'POST') {
    let body;
    try { body = await readJson(req); } catch (e) { return send(res, 400, { error: e.message }); }
    if (MODE === 'vuln') {
      try {
        const r = await fetch(body.url);                         // any scheme, any host, follows redirects
        return send(res, 200, { status: r.status, body: await r.text() });   // raw response back to the client
      } catch (e) { return send(res, 502, { error: String(e.cause?.code || e.message) }); }
    }
    let target;
    try { target = new URL(body.url); } catch { return send(res, 400, { error: 'invalid url' }); }
    if (target.protocol !== 'http:' || !ALLOWED_AVATAR_HOSTS.has(target.host) || target.username || target.password) {
      audit('ssrf_blocked', { user: user.id, url: String(body.url).slice(0, 200) });
      return send(res, 400, { error: 'host not allowed' });
    }
    try {
      const r = await fetch(target, { redirect: 'manual', signal: AbortSignal.timeout(3000) });
      const type = r.headers.get('content-type') || '';
      if (r.status !== 200 || !type.startsWith('image/')) {
        audit('ssrf_blocked', { user: user.id, url: target.href, status: r.status, type });
        return send(res, 400, { error: 'not an image' });
      }
      const bytes = (await r.arrayBuffer()).byteLength;
      if (bytes > 1_000_000) return send(res, 400, { error: 'image too large' });
      return send(res, 200, { stored: true, content_type: type, bytes });   // no raw body back
    } catch { return send(res, 502, { error: 'fetch failed' }); }
  }

  return send(res, 404, { error: 'not found' });
});
server.listen(8000, () => console.error(JSON.stringify({ event: 'started', mode: MODE })));
```

والملف `internal.mjs` للخدمتين الداخليتين، وفيه نقطة `/go` في خدمة الصور تعيد التوجيه إلى أي رابط، حتى نرى لماذا يجب ألا يتبع الجالب إعادة التوجيه:

```javascript
// Two internal lab services, chosen by ROLE:
//   meta    : imitates a cloud metadata service, reachable only inside the lab network
//   avatars : the one allowed image host; it also has an open redirect, /go?to=...
import http from 'node:http';

const ROLE = process.env.ROLE;
const PNG = Buffer.from('89504e470d0a1a0a0000000d4948445200000001000000010806000000' +
                        '1f15c4890000000d49444154789c6360000002000154a24f5d0000000049454e44ae426082', 'hex');

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://internal');
  if (ROLE === 'meta' && url.pathname === '/latest/meta-data/iam/credentials') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ AccessKeyId: 'LAB-ONLY-KEY', SecretAccessKey: 'lab-only-not-a-real-secret' }));
  }
  if (ROLE === 'avatars' && url.pathname === '/alice.png') {
    res.writeHead(200, { 'Content-Type': 'image/png' });
    return res.end(PNG);
  }
  if (ROLE === 'avatars' && url.pathname === '/go') {
    res.writeHead(302, { Location: url.searchParams.get('to') || '/' });
    return res.end();
  }
  res.writeHead(404); res.end();
}).listen(8080);
```

في الملفين أعلاه لاحظ التالي:

- الدالة `publicUser` هي نموذج الرد DTO، والمجموعة `EDITABLE` هي قائمة السماح للحقول، والمجموعة `ALLOWED_AVATAR_HOSTS` هي قائمة السماح للمضيفين، وكلها لا تستخدم إلا في الوضع المصلح.
- كل رفض في الوضع المصلح يكتب سطراً منظماً بصيغة JSON في السجل، فيه المستخدم وما طلبه، وهذا هو الرابط مع السجلات في [السجلات المنظمة Structured Logs](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#structured-logs).
- الوضع الضعيف في `/avatar/fetch` يستخدم `fetch` بإعداداتها الافتراضية، وهي تتبع إعادة التوجيه، ويعيد الرد الخام، وهذا ما يجعل الثغرة تكشف السر.

### تشغيل ال Containers

شغلنا التجربة على Docker Engine 29.8.0، والملفات تحتاج Node.js 22 فقط، ولأن مساحة القرص على جهاز الاختبار كانت محدودة استخدمنا Node.js 22.23.3 الموجودة داخل Docker Image كانت على الجهاز بدل تنزيل Image جديدة، وتستطيع أنت أن تضع مكانها `node:22-alpine` الرسمية بنفس الأوامر. ومن المجلد الذي فيه الملفات:

```bash
IMG=ghost:6-alpine            # any image with Node.js 22, for example node:22-alpine
docker network create lab-api-net
docker run -d --name lab-api-meta    --network lab-api-net --network-alias meta    --user node \
  -e ROLE=meta    -v "$PWD:/lab:ro" --entrypoint node $IMG /lab/internal.mjs
docker run -d --name lab-api-avatars --network lab-api-net --network-alias avatars --user node \
  -e ROLE=avatars -v "$PWD:/lab:ro" --entrypoint node $IMG /lab/internal.mjs
docker run -d --name lab-api-vuln  --network lab-api-net -e MODE=vuln  -p 127.0.0.1:20780:8000 \
  --user node -v "$PWD:/lab:ro" --entrypoint node $IMG /lab/app.mjs
docker run -d --name lab-api-fixed --network lab-api-net -e MODE=fixed -p 127.0.0.1:20781:8000 \
  --user node -v "$PWD:/lab:ro" --entrypoint node $IMG /lab/app.mjs
docker ps --filter name=^lab-api- --format '{{.Names}} {{.Status}} {{.Ports}}'
```

والمخرج سوف يكون كما يلي:

```text
lab-api-fixed Up 3 seconds 127.0.0.1:20781->8000/tcp
lab-api-vuln Up 4 seconds 127.0.0.1:20780->8000/tcp
lab-api-avatars Up 5 seconds 2368/tcp
lab-api-meta Up 7 seconds 2368/tcp
```

لاحظ أن الخدمتين الداخليتين لا يوجد لهما أي منفذ منشور على الجهاز، والقيمة `2368/tcp` منفذ معلن في ال Image نفسها وليس منشوراً، فلا يصل إليهما إلا من كان على الشبكة `lab-api-net`، تماماً كخدمة البيانات الوصفية التي لا يصل إليها إلا من كان داخل السيرفر.

### النسخة الضعيفة: BOLA وBOPLA وBFLA

نبدأ بالمتغيرات التي سوف نستخدمها، ثم تطلب alice طلبها رقم 7001، ثم طلب bob رقم 7002، ثم نطلب دون توكن:

```bash
V=http://127.0.0.1:20780
A="Authorization: Bearer tok-alice"; B="Authorization: Bearer tok-bob"; W='  HTTP %{http_code}\n'
curl -s -w "$W" -H "$A" $V/orders/7001
curl -s -w "$W" -H "$A" $V/orders/7002
curl -s -w "$W" $V/orders/7002
```

والمخرج سوف يكون كما يلي:

```text
{"id":7001,"owner":"alice","item":"item-1","total":47,"address":"1 King Fahd Rd, Riyadh"}  HTTP 200
{"id":7002,"owner":"bob","item":"item-2","total":84,"address":"2 King Fahd Rd, Riyadh"}  HTTP 200
{"error":"login required"}  HTTP 401
```

المصادقة تعمل، فالطلب دون توكن رفض، ومن يختبر ال API بهذا الطلب وحده سوف يظن أن كل شيء سليم، ولكن alice قرأت طلب bob وعنوانه، وهذا هو BOLA. والآن تقرأ alice ملفها الشخصي، ثم تستدعي نقطة تصدير المستخدمين الخاصة بالإدارة:

```bash
curl -s -w "$W" -H "$A" $V/users/me
curl -s -w "$W" -H "$A" $V/admin/users
```

والمخرج سوف يكون كما يلي:

```text
{"id":"alice","display_name":"Alice","phone":"0500000001","role":"user","is_admin":false,"password_hash":"$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA"}  HTTP 200
[{"id":"alice","display_name":"Alice","phone":"0500000001","role":"user","is_admin":false,"password_hash":"$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA"},{"id":"bob","display_name":"Bob","phone":"0500000002","role":"user","is_admin":false,"password_hash":"$argon2id$v=19$m=19456,t=2,p=1$c2FsdDI$aGFzaDI"},{"id":"carol","display_name":"Carol","phone":"0500000003","role":"admin","is_admin":true,"password_hash":"$argon2id$v=19$m=19456,t=2,p=1$c2FsdDM$aGFzaDM"}]  HTTP 200
```

في السطر الأول خرج الحقل `password_hash` مع الملف الشخصي، وهو كشف البيانات الزائد في API3، وفي السطر الثاني صدرت alice، وهي مستخدمة عادية، كل المستخدمين مع هاش كلمات مرورهم، وهذا BFLA وBOPLA معاً. والهاش في التجربة وهمي، ولكنه في تطبيق حقيقي يعطي المخترق ما يحتاجه ليخمن كلمات المرور على جهازه دون أي حد للمحاولات. والآن يرسل bob طلب تعديل فيه حقل لم يكن في الواجهة أصلاً:

```bash
curl -s -w "$W" -H "$B" -X PATCH -H "Content-Type: application/json" \
  -d '{"display_name":"Bob","is_admin":true}' $V/users/me
```

والمخرج سوف يكون كما يلي:

```text
{"id":"bob","display_name":"Bob","phone":"0500000002","role":"admin","is_admin":true,"password_hash":"$argon2id$v=19$m=19456,t=2,p=1$c2FsdDI$aGFzaDI"}  HTTP 200
```

أصبح bob مديراً بطلب واحد، والسبب سطر `Object.assign(user, body)` الذي ينسخ كل ما يرسله الكلاينت، وهذا هو الإسناد الجماعي Mass Assignment.

### النسخة الضعيفة: صفحة بلا حد، وSSRF

نطلب قائمة الطلبات مرتين، مرة بالحجم الافتراضي ومرة بحجم يختاره الكلاينت، ونطبع حجم الرد وزمنه بدل محتواه:

```bash
curl -s -o /dev/null -w "limit=20      HTTP %{http_code}  %{size_download} bytes  %{time_total}s\n" -H "$A" "$V/orders?limit=20"
curl -s -o /dev/null -w "limit=100000  HTTP %{http_code}  %{size_download} bytes  %{time_total}s\n" -H "$A" "$V/orders?limit=100000"
```

والمخرج سوف يكون كما يلي:

```text
limit=20      HTTP 200  1943 bytes  0.011110s
limit=100000  HTTP 200  10348579 bytes  0.379222s
```

رد واحد بحجم 10 ميجابايت تقريباً، فيه طلبات كل العملاء وعناوينهم، فالخطر هنا BOLA على مستوى القائمة أيضاً. والزمن 0.38 ثانية يبدو قليلاً، فماذا يحدث إذا أرسل سكربت عشرين طلباً مثله في نفس الوقت؟ نقيس ذاكرة ال Container قبل الطلبات وبعدها:

```bash
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' lab-api-vuln
time (seq 20 | xargs -P 20 -I{} curl -s -o /dev/null -H "$A" "$V/orders?limit=100000")
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' lab-api-vuln
```

والمخرج سوف يكون كما يلي:

```text
lab-api-vuln 76.93MiB / 31.29GiB

real	0m7.433s
user	0m0.183s
sys	0m0.334s
lab-api-vuln 362MiB / 31.29GiB
```

عشرون طلباً فقط رفعت الذاكرة من 77 إلى 362 ميجابايت، واستغرقت 7.4 ثانية كان ال API خلالها مشغولاً عن غيرها، وهذه أرقام من جهاز الاختبار للتوضيح وليست قياساً للأداء، ولكن الفكرة واضحة، فالمهاجم لا يحتاج آلاف الطلبات إذا كان كل طلب يطلب كل شيء. والآن ميزة جلب الصورة، مرة بالرابط الصحيح، ومرة برابط الخدمة الداخلية، ومرة برابط خدمة الصور نفسها الذي يعيد التوجيه إلى الخدمة الداخلية:

```bash
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" \
  -d '{"url":"http://avatars:8080/alice.png"}' $V/avatar/fetch | cut -c1-120
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" \
  -d '{"url":"http://meta:8080/latest/meta-data/iam/credentials"}' $V/avatar/fetch
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" \
  -d '{"url":"http://avatars:8080/go?to=http://meta:8080/latest/meta-data/iam/credentials"}' $V/avatar/fetch
```

والمخرج سوف يكون كما يلي:

```text
{"status":200,"body":"�PNG\r\n\u001a\n\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0006
{"status":200,"body":"{\"AccessKeyId\":\"LAB-ONLY-KEY\",\"SecretAccessKey\":\"lab-only-not-a-real-secret\"}"}  HTTP 200
{"status":200,"body":"{\"AccessKeyId\":\"LAB-ONLY-KEY\",\"SecretAccessKey\":\"lab-only-not-a-real-secret\"}"}  HTTP 200
```

في المخرج أعلاه لاحظ التالي:

- الخدمة `meta` لا يصل إليها جهازنا أصلاً، ولكن ال API وصل إليها لأنه على نفس الشبكة، ثم أعاد ردها إلينا كما هو، وهذا بالضبط ما تقصده OWASP بقولها إن الطلب يتجاوز جدار الحماية.
- الطلب الثالث بدأ من المضيف المسموح نفسه، ثم اتبع إعادة التوجيه إلى الخدمة الداخلية، ولذلك فقائمة السماح وحدها لا تكفي إذا كان الجالب يتبع إعادة التوجيه.
- حتى الطلب الأول الصحيح أعاد الملف الخام داخل JSON، وهذا يجعل ال API قناة لنقل أي محتوى، وهو ما تمنعه التوصية «لا ترسل الردود الخام».

### النسخة المصلحة: نفس الطلبات

نعيد الطلبات نفسها على المنفذ 20781، ومعها طلب لرقم غير موجود، وطلب carol المديرة، وتعديل مسموح من bob:

```bash
V=http://127.0.0.1:20781; C="Authorization: Bearer tok-carol"
curl -s -w "$W" -H "$A" $V/orders/7001
curl -s -w "$W" -H "$A" $V/orders/7002
curl -s -w "$W" -H "$A" $V/orders/999999
curl -s -w "$W" -H "$A" $V/users/me
curl -s -w "$W" -H "$A" $V/admin/users
curl -s -w "$W" -H "$C" $V/admin/users
curl -s -w "$W" -H "$B" -X PATCH -H "Content-Type: application/json" -d '{"display_name":"Bob","is_admin":true}' $V/users/me
curl -s -w "$W" -H "$B" -X PATCH -H "Content-Type: application/json" -d '{"display_name":"Bob B."}' $V/users/me
```

والمخرج سوف يكون كما يلي:

```text
{"id":7001,"owner":"alice","item":"item-1","total":47,"address":"1 King Fahd Rd, Riyadh"}  HTTP 200
{"error":"not found"}  HTTP 404
{"error":"not found"}  HTTP 404
{"id":"alice","display_name":"Alice","phone":"0500000001"}  HTTP 200
{"error":"forbidden"}  HTTP 403
[{"id":"alice","display_name":"Alice","phone":"0500000001","role":"user"},{"id":"bob","display_name":"Bob","phone":"0500000002","role":"user"},{"id":"carol","display_name":"Carol","phone":"0500000003","role":"admin"}]  HTTP 200
{"error":"fields not allowed: is_admin"}  HTTP 400
{"id":"bob","display_name":"Bob B.","phone":"0500000002"}  HTTP 200
```

طلب bob والرقم غير الموجود أعادا الرد نفسه، والملف الشخصي خرج بثلاثة حقول فقط، وalice رفضت بالرمز 403 من نقطة الإدارة بينما نجحت carol، وحتى carol لا ترى الهاش لأن رد الإدارة له نموذجه أيضاً، والحقل `is_admin` رفض باسمه بينما مر التعديل المسموح. والآن حجم الصفحة وجلب الصورة، ومعهما عنوان البيانات الوصفية الحقيقي في السحابة ورابط بمخطط `file`:

```bash
curl -s -o /dev/null -w "limit=20      HTTP %{http_code}  %{size_download} bytes  %{time_total}s\n" -H "$A" "$V/orders?limit=20"
curl -s -w "$W" -H "$A" "$V/orders?limit=100000"
curl -s -w "$W" -H "$A" "$V/orders?limit=20"
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" -d '{"url":"http://avatars:8080/alice.png"}' $V/avatar/fetch
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" -d '{"url":"http://meta:8080/latest/meta-data/iam/credentials"}' $V/avatar/fetch
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" -d '{"url":"http://169.254.169.254/latest/meta-data/"}' $V/avatar/fetch
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" -d '{"url":"http://avatars:8080/go?to=http://meta:8080/latest/meta-data/iam/credentials"}' $V/avatar/fetch
curl -s -w "$W" -H "$A" -H "Content-Type: application/json" -d '{"url":"file:///etc/passwd"}' $V/avatar/fetch
```

والمخرج سوف يكون كما يلي:

```text
limit=20      HTTP 200  111 bytes  0.009502s
{"error":"limit must be between 1 and 100"}  HTTP 400
{"count":1,"items":[{"id":7001,"owner":"alice","item":"item-1","total":47,"address":"1 King Fahd Rd, Riyadh"}]}  HTTP 200
{"stored":true,"content_type":"image/png","bytes":66}  HTTP 200
{"error":"host not allowed"}  HTTP 400
{"error":"host not allowed"}  HTTP 400
{"error":"not an image"}  HTTP 400
{"error":"host not allowed"}  HTTP 400
```

الصفحة الكبيرة رفضت برسالة تقول الحد، والقائمة أصبحت تحتوي طلب alice وحده، والصورة الصحيحة حفظت ولم يعد محتواها، وكل الروابط الداخلية رفضت قبل أي اتصال، أما رابط إعادة التوجيه فمر من قائمة السماح لأن مضيفه مسموح، ثم رفض لأن الجالب لم يتبع الرد 302 ووجد أنه ليس صورة. وكل رفض كتب سطراً في السجل:

```bash
docker logs lab-api-fixed
```

والمخرج سوف يكون كما يلي:

```text
{"event":"started","mode":"fixed"}
{"event":"access_denied","user":"alice","object":"order:7002"}
{"event":"access_denied","user":"alice","object":"order:999999"}
{"event":"function_denied","user":"alice","function":"GET /admin/users"}
{"event":"rejected_fields","user":"bob","fields":["is_admin"]}
{"event":"ssrf_blocked","user":"alice","url":"http://meta:8080/latest/meta-data/iam/credentials"}
{"event":"ssrf_blocked","user":"alice","url":"http://169.254.169.254/latest/meta-data/"}
{"event":"ssrf_blocked","user":"alice","url":"http://avatars:8080/go?to=http://meta:8080/latest/meta-data/iam/credentials","status":302,"type":""}
{"event":"ssrf_blocked","user":"alice","url":"file:///etc/passwd"}
```

ولاحظ أن هذه السطور هي ما يحتاجه فريق التشغيل، فمستخدم واحد ظهر في السجل بخمسة أنواع من الرفض في دقيقة واحدة يستحق تنبيهاً فورياً، وقد شرحنا كيف يتحول السجل إلى تنبيه وأثر لا يمحى في دليل [سجل التدقيق Audit Trail](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/).

### اختبارات الصلاحيات Authorization Tests: تفشل ثم تنجح

ما رأيناه بأوامر curl يجب ألا يعتمد على أن يتذكره أحد قبل كل نشر، لذلك نكتبه اختبارات آلية بأداة الاختبار المدمجة في Node.js [node:test](https://nodejs.org/api/test.html?ref=arabroot.io)، وكل اختبار يرسل طلباً يجب أن يرفضه ال API، والملف `authz.test.mjs` كاملاً كما يلي:

```javascript
// Authorization tests: each one sends a request the API must refuse.  BASE=http://host:port node --test
import { test } from 'node:test';
import assert from 'node:assert/strict';

const BASE = process.env.BASE;
const call = async (token, method, path, body) => {
  const r = await fetch(BASE + path, {
    method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: body && JSON.stringify(body),
  });
  return { status: r.status, json: await r.json() };
};

test('API1 alice cannot read bob\'s order', async () => {
  const r = await call('tok-alice', 'GET', '/orders/7002');
  assert.equal(r.status, 404);
});

test('API3 the profile never returns password_hash', async () => {
  const r = await call('tok-alice', 'GET', '/users/me');
  assert.equal(r.json.password_hash, undefined);
});

test('API3 a user cannot make himself admin with PATCH', async () => {
  await call('tok-bob', 'PATCH', '/users/me', { is_admin: true });
  const r = await call('tok-bob', 'GET', '/admin/users');
  assert.equal(r.status, 403);
});

test('API5 a regular user cannot call the admin export', async () => {
  const r = await call('tok-alice', 'GET', '/admin/users');
  assert.equal(r.status, 403);
});

test('API4 the page size has a maximum', async () => {
  const r = await call('tok-alice', 'GET', '/orders?limit=100000');
  assert.equal(r.status, 400);
});

test('API7 the avatar fetcher refuses internal hosts', async () => {
  const r = await call('tok-alice', 'POST', '/avatar/fetch', { url: 'http://meta:8080/latest/meta-data/iam/credentials' });
  assert.equal(r.status, 400);
});
```

نعيد تشغيل النسختين حتى تعود البيانات إلى حالتها الأولى، ثم نشغل الاختبارات من Container على نفس الشبكة، مرة على النسخة الضعيفة ومرة على المصلحة:

```bash
docker restart lab-api-vuln lab-api-fixed
for t in vuln fixed; do
  echo "== $t"
  docker run --rm --name lab-api-tests --network lab-api-net --user node -e BASE=http://lab-api-$t:8000 \
    -v "$PWD:/lab:ro" -w /lab --entrypoint node $IMG --test --test-reporter=spec authz.test.mjs
  echo "exit=$?"
done
```

والمخرج مختصراً، بعد حذف أسطر ال Stack Trace وتفاصيل الاختبارات الفاشلة بعد الأول، سوف يكون كما يلي:

```text
== vuln
✖ API1 alice cannot read bob's order (117.932675ms)
✖ API3 the profile never returns password_hash (14.131243ms)
✖ API3 a user cannot make himself admin with PATCH (48.143715ms)
✖ API5 a regular user cannot call the admin export (15.88246ms)
✖ API4 the page size has a maximum (361.269223ms)
✖ API7 the avatar fetcher refuses internal hosts (34.129179ms)
ℹ tests 6
ℹ suites 0
ℹ pass 0
ℹ fail 6
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 751.843204
✖ failing tests:
test at authz.test.mjs:14:1
✖ API1 alice cannot read bob's order (117.932675ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  200 !== 404
exit=1
== fixed
✔ API1 alice cannot read bob's order (143.17496ms)
✔ API3 the profile never returns password_hash (12.055505ms)
✔ API3 a user cannot make himself admin with PATCH (18.478034ms)
✔ API5 a regular user cannot call the admin export (4.65001ms)
✔ API4 the page size has a maximum (4.922528ms)
✔ API7 the avatar fetcher refuses internal hosts (10.664412ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 529.70087
exit=0
```

في المخرج أعلاه لاحظ التالي:

- الاختبارات الستة فشلت على النسخة الضعيفة ونجحت على المصلحة، ورمز الخروج 1 ثم 0، وهذا كل ما يحتاجه خط CI حتى يمنع الدمج، وهذا تطبيق حرفي لتوصية API1: لا تنشر تغييراً يفشل اختبارات التفويض.
- اختبار حجم الصفحة على النسخة الضعيفة استغرق 361 ميلي ثانية، وهو أبطأ اختبار، لأنه أعاد 10 ميجابايت.
- هذه الاختبارات لا تحتاج أداة أمنية، فهي اختبارات تكامل Integration Tests عادية، وقد شرحنا مكانها بين أنواع الاختبارات في دليل [أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#api-tests)، وفي مشروع حقيقي تكتب بنفس لغة المشروع، xUnit في C# أو JUnit في Java أو pytest في Python.

### إزالة التجربة

بعد التجربة حذفنا ال Containers والشبكة التي أنشأناها بأسمائها فقط:

```bash
docker rm -f lab-api-vuln lab-api-fixed lab-api-meta lab-api-avatars
docker network rm lab-api-net
```

## بوابات ال API Gateway: ماذا تفعل، وما الذي لا تستطيع فعله؟

بوابة ال API Gateway برنامج يقف أمام واجهاتك مثل ال Reverse Proxy، ولكنه مصمم لل APIs، فيتحقق من التوكن أو المفتاح، ويطبق حد المعدل وحد الحجم، ويوجه كل نسخة إلى خدمتها، ويسجل كل طلب في مكان واحد. وهذا يغطي جزءاً حقيقياً من API2 وAPI4 وAPI8 وAPI9، ولكن البوابة لا تقرأ قاعدة بياناتك، فلا تعرف أن الطلب 7002 ليس لalice، ولا أن الحقل `is_admin` لا يجب أن يتغير، وبالتالي فهي لا تحميك من BOLA ولا من BOPLA، وقد شرحنا هذا الحد بالتفصيل في قسم [أين يكون الفحص: البوابة أم التطبيق؟](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/#where-checks). وأشهر الخيارات المفتوحة ما يلي، والمعلومات من مستودعاتها حتى أكتوبر 2026:

| الأداة                                                              | الترخيص    | ما يجب أن تعرفه                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Kong Gateway](https://github.com/Kong/kong?ref=arabroot.io)        | Apache 2.0 | آخر إصدار في المستودع المفتوح 3.9.3 في يونيو 2026، بينما تذكر [سياسة الدعم](https://developer.konghq.com/gateway/version-support-policy/?ref=arabroot.io) إصدارات أحدث حتى 3.16 بصفتها Enterprise، فتحقق من الإصدار المتاح لك قبل أن تبني عليه                                                                      |
| [Apache APISIX](https://github.com/apache/apisix?ref=arabroot.io)   | Apache 2.0 | مشروع من مؤسسة Apache، وآخر إصدار 3.19.0 في سبتمبر 2026، ويحمل الإعداد في etcd أو في ملف                                                                                                                                                                                                                            |
| [Traefik Proxy](https://github.com/traefik/traefik?ref=arabroot.io) | MIT        | Reverse Proxy أكثر منه بوابة، ولكن فيه [Middleware لحد المعدل](https://doc.traefik.io/traefik/middlewares/http/ratelimit/?ref=arabroot.io) وForward Auth، وآخر إصدار v3.7.14 في أكتوبر 2026، وقد شرحنا تثبيته في [دليل Traefik](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-traefik-reverse-proxy/) |

والقاعدة العملية أن تضع في البوابة ما هو عام ورخيص، مثل التحقق من التوكن وحد المعدل وحجم الطلب والتوجيه حسب النسخة، وتترك في التطبيق كل قرار يحتاج إلى البيانات، ولا تقم بإزالة الفحص من التطبيق لأن «البوابة تفحص»، والسبب أن الطلب الذي يصل إلى الخدمة من داخل الشبكة دون أن يمر بالبوابة يجب أن يرفض أيضاً.

## ملف OpenAPI: العقد Contract والسجل Inventory معاً

مواصفة [OpenAPI](https://spec.openapis.org/oas/v3.2.1.html?ref=arabroot.io)، وآخر إصدار منها 3.2.1 في سبتمبر 2026، تصف ال API في ملف YAML أو JSON: النقاط والطرق والمعاملات وأنواعها وحدودها والردود ومخططاتها وطريقة المصادقة. وهذا الملف أهم أداة أمنية لا يراها أحد أداة أمنية، والسبب أنه يخدم أربعة أغراض في وقت واحد:

- **السجل Inventory:** كل Endpoint موجود في الملف، فما ليس فيه لا يجب أن يعمل، وهذا جواب API9.
- **العقد Contract:** الحدود مكتوبة فيه، مثل `maximum: 100` لحجم الصفحة و`maxLength` للنصوص و`additionalProperties: false` للحقول، فتتحقق منها البوابة أو مكتبة التحقق في التطبيق قبل أن يصل الطلب إلى الكود، وهذا جواب جزء من API3 وAPI4.
- **الاختبار:** اختبارات العقد Contract Tests تتأكد أن الرد يطابق المخطط، فإذا ظهر فيه حقل جديد مثل `password_hash` فشل الاختبار، كما في قسم [اختبار العقد Contract Test](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#contract).
- **الفحص:** أداة ZAP تقرأ الملف وتفحص كل نقطة فيه، لأن العنكبوت Spider لا يجد نقاط ال API في الصفحات، وقد رأينا ذلك في قسم [فحص ال API من ملف OpenAPI](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D8%AB%D9%86%D8%A7%D8%A1-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-dast-%D9%85%D8%B9-zap/#lab-api).

والمثال التالي يبين كيف تكتب الحدود في الملف لنقطتين من متجرنا:

```yaml
paths:
  /orders:
    get:
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: after_id, in: query, schema: { type: integer } }
  /users/me:
    patch:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false        # is_admin is rejected by the contract itself
              properties:
                display_name: { type: string, maxLength: 80 }
                phone: { type: string, maxLength: 20 }
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicUser" }
```

ولا تكتب الملف يدوياً إذا كان إطار العمل يولده من الكود، فهذا يضمن أن الملف يطابق ما يعمل فعلاً: FastAPI يولده تلقائياً من الأنواع، وASP.NET Core يولده بالحزمة Microsoft.AspNetCore.OpenApi كما في [توثيق OpenAPI في Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/openapi?ref=arabroot.io)، وفي Spring تستخدم مكتبة [springdoc-openapi](https://springdoc.org/?ref=arabroot.io). ثم اجعل خط CI يحفظ الملف ويقارنه بالنسخة السابقة، فكل Endpoint جديد يظهر في طلب الدمج Pull Request ويراه المراجع.

### ترقيم الصفحات Pagination والحدود Limits

كل Endpoint يعيد قائمة يحتاج ثلاثة قرارات مكتوبة: حجم افتراضي صغير، وحد أقصى يرفض ما فوقه، وطريقة ترقيم لا تثقل قاعدة البيانات. والترقيم بالإزاحة `OFFSET` سهل ولكنه يبطئ كلما تقدمت الصفحة، والترقيم بالمؤشر Cursor أو Keyset، كما في مثال API4، ثابت السرعة ويصعب معه القفز إلى صفحة بعيدة، وهذا غالباً ما تريده في ال API. ولا تقم بإرجاع العدد الكلي `total` في كل صفحة إذا لم يحتجه الكلاينت، والسبب أن `COUNT(*)` على جدول كبير قد يكون أغلى من الصفحة نفسها، فيتحول إلى طريق آخر لاستهلاك الموارد.

### النسخ Versioning والإيقاف Deprecation

النسخة في الرابط، مثل `/v2/orders`، هي الأوضح في السجلات وفي البوابة، ولكن أياً كانت الطريقة فالمهم ما يحدث للنسخة القديمة، وهنا يلتقي الأمن بالتشغيل، لأن كل نسخة تعمل هي نسخة يجب أن تصلحها. والخطوات المعقولة: أعلن الإيقاف في التوثيق وفي ملف OpenAPI بالحقل `deprecated: true`، وأرسل الترويستين `Deprecation` و`Sunset` كما في [مثال API9](#api9-code)، وراقب في السجلات من ما زال يستدعي النسخة القديمة وأخبره، ثم احذفها فعلاً في التاريخ المعلن، أي المسار في البوابة والكود وسجل DNS وال Host، وليس المسار وحده.

## وماذا عن GraphQL؟

في GraphQL يوجد Endpoint واحد عادة، والكلاينت يكتب الاستعلام الذي يريده، وهذا يغير شكل بعض المخاطر ولا يلغيها:

- **استهلاك الموارد API4:** استعلام متداخل بعمق كبير، أو استعلام يطلب آلاف العناصر، أو عشرات العمليات في طلب واحد Batching يتجاوز حد المعدل لأنه طلب HTTP واحد، ولذلك يوصي [OWASP GraphQL Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL%5FCheat%5FSheet.html?ref=arabroot.io) بحد للعمق Depth Limiting وحد للكمية Amount Limiting وتحليل تكلفة الاستعلام Query Cost Analysis مع حد أقصى للتكلفة، ويذكر أن هذه الحدود لا تأتي مع GraphQL نفسه وتحتاج إعداداً أو مكتبة.
- **الاستكشاف Introspection:** ميزة تجعل ال API يصف مخططه كاملاً، ويوصي نفس المرجع بتعطيلها في البيئات العامة والإنتاج، ويقول [توثيق GraphQL نفسه](https://graphql.org/learn/introspection/?ref=arabroot.io) إنها غالباً غير لازمة في الإنتاج إذا كان ال API لتطبيقاتك أنت، ولكن تذكر أن إخفاء المخطط ليس صلاحيات.
- **BOLA وBOPLA في كل Resolver:** كل حقل في GraphQL دالة، والفحص الذي وضعته في استعلام `order(id)` يجب أن يوجد أيضاً في `customer { orders }`، والحقل الحساس يحتاج قاعدة تفويض على الحقل نفسه، لأن الكلاينت يستطيع أن يصل إليه من أكثر من طريق.

## ال Webhooks: API يطلبك أنت

ال Webhook طلب HTTP ترسله خدمة أخرى إلى تطبيقك عند حدث ما، مثل بوابة الدفع التي تخبرك أن الطلب دفع، وهو Endpoint علني يجب أن يثبت أن الطلب جاء فعلاً من المرسل وأنه لم يتغير، والطريقة المعيارية توقيع HMAC بمفتاح مشترك، وقد شرحنا HMAC ولماذا لا يكفي الهاش وحده ولماذا تكون المقارنة في زمن ثابت في قسم [HMAC والمقارنة في زمن ثابت](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD/#hmac-right-way)، فلا نعيده هنا. وما يخص ال Webhooks تحديداً ثلاث نقاط: احسب التوقيع على الجسم الخام Raw Body كما وصل وليس بعد تحويله إلى كائن ثم إعادته إلى نص، وأدخل في التوقيع وقت الإرسال Timestamp وارفض الطلب القديم حتى لا يعاد إرسال طلب صحيح مرتين Replay، ثم تعامل مع كل حدث بحيث لا يتكرر أثره إذا وصل مرتين Idempotency. وتجد مثالاً عملياً في [توثيق GitHub للتحقق من ال Webhooks](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries?ref=arabroot.io)، وصيغة موحدة للتوقيع والترويسات في مواصفة [Standard Webhooks](https://www.standardwebhooks.com/?ref=arabroot.io).

والجهة الأخرى أخطر، فإذا كان تطبيقك يرسل Webhooks إلى روابط يكتبها العملاء، فأنت تبني جالب روابط، أي ميزة SSRF كما في API7، فطبق عليه نفس الفحوص: قائمة منع للعناوين الداخلية بعد حل الاسم، ولا تتبع إعادة التوجيه، ومهلة قصيرة، ولا تعرض للعميل رد الرابط الخام.

## أين تكتشف كل خطر؟

بعد أن مررنا على المخاطر العشر، الجدول التالي يجمع المكان الأقوى لاكتشاف كل منها، وهو تقدير تحريري منا مبني على أقسام المنع في صفحات OWASP وليس بيانات منشورة:

| الخطر                    | اختبارات تكتبها أنت        | فحص DAST من OpenAPI   | السجلات والمقاييس           | المراجعة ونمذجة التهديدات |
| ------------------------ | -------------------------- | --------------------- | --------------------------- | ------------------------- |
| API1 BOLA                | الأقوى: طلب مستخدم آخر     | يحتاج حسابين وسيناريو | تكرار access\_denied        | نعم                       |
| API2 Authentication      | توكن منتهي ومزور           | جزئياً                | فشل الدخول المتكرر          | نعم                       |
| API3 BOPLA               | غياب الحقول ورفض الممنوع   | نادراً                | rejected\_fields            | نعم                       |
| API4 Resources           | الحد الأقصى، واختبار الضغط | جزئياً                | حجم الردود وزمنها           | جزئياً                    |
| API5 BFLA                | جدول الأدوار والنقاط       | يحتاج حسابين          | function\_denied            | نعم                       |
| API6 Business Flows      | القاعدة التجارية           | لا                    | الأقوى: أنماط الاستخدام     | الأقوى                    |
| API7 SSRF                | عنوان داخلي مرفوض          | جزئياً                | الطلبات الخارجة Egress      | نعم                       |
| API8 Misconfiguration    | جزئياً                     | الأقوى                | جزئياً                      | نعم                       |
| API9 Inventory           | مقارنة الملف بالمسارات     | يفحص ما في الملف فقط  | الأقوى: ما لا يوجد في السجل | نعم                       |
| API10 Unsafe Consumption | ردود خارجية غريبة          | لا                    | المهلات والردود المرفوضة    | الأقوى                    |

ولاحظ أن عمود الاختبارات التي تكتبها أنت هو الأقوى في مخاطر الصلاحيات الثلاثة، وأن أداة الفحص الآلي تجيد الإعدادات وتضعف في كل ما يحتاج معرفة بقواعد عملك، وهذا نفس ما رأيناه في قائمة الويب في قسم [أين تكتشف كل خطر](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#where-caught).

## أخطاء شائعة Common Mistakes

- **الاعتماد على الواجهة:** «التطبيق لا يعرض الزر» أو «التطبيق لا يرسل هذا الحقل»، وال API يقبل الطلب من أي كلاينت.
- **المصادقة بدل التفويض:** `[Authorize]` أو `authenticated()` على كل شيء، ثم لا يوجد أي سؤال عن الكائن أو الوظيفة.
- **إرجاع الكيان كما هو:** الرد يحمل كل أعمدة الجدول، والطلب يملأ كل أعمدة الجدول.
- **المعرف العشوائي كحماية:** UUID يصعب التخمين، ولكنه يتسرب، ولا يغني عن فحص الملكية.
- **حد المعدل وحده:** بوابة تسمح بمئة طلب في الدقيقة، وكل طلب يعيد مليون سجل.
- **رمز 503 لحد المعدل:** الكلاينت يفهم أن السيرفر معطل فيعيد المحاولة فوراً، والصحيح 429 مع `Retry-After`.
- **قائمة منع في SSRF:** بدل قائمة سماح، مع تتبع إعادة التوجيه.
- **النسخة القديمة «للتوافق فقط»:** تعمل بلا مالك ولا مراقبة ولا تحديثات، وتقرأ من قاعدة الإنتاج.
- **الثقة بالخدمة الخارجية:** ردها يدخل قاعدة البيانات أو الواجهة دون تحقق ودون مهلة.
- **اختبار ال API بحساب واحد:** كل الاختبارات تنجح لأن المستخدم الوحيد يملك كل شيء.

## قائمة تحقق Checklist لكل Endpoint جديد

- هل يفحص السيرفر أن الكائن الذي يحمل معرفه الطلب مملوك للمستخدم، في الاستعلام نفسه، مع اختبار بتوكن مستخدم آخر يتوقع 404؟ (API1)
- هل التوكن يتحقق منه بالتوقيع والخوارزمية والجهة المصدرة والجمهور والانتهاء، وعلى نقطة الدخول حد أشد؟ (API2)
- هل للرد نموذج DTO يختار الحقول، وللطلب نموذج بقائمة سماح يرفض ما سواها؟ (API3)
- هل لحجم الصفحة حد أقصى، وللنصوص والمصفوفات والملفات حدود، وللخدمات المدفوعة سقف إنفاق، ولل Container حد للذاكرة؟ (API4)
- هل الوظيفة مسموحة لدور محدد، والرفض هو الأصل لكل ما لم يذكر، بما في ذلك طرق HTTP الأخرى؟ (API5)
- هل سأل أحد «ماذا لو استخدم سكربت هذا المسار ألف مرة؟»، وهل القاعدة التجارية على السيرفر؟ (API6)
- إذا كان ال Endpoint يجلب رابطاً، هل هناك قائمة سماح ولا يتبع إعادة التوجيه ولا يعيد الرد الخام؟ (API7)
- هل الإنتاج يعمل بإعدادات الإنتاج، والأخطاء بصيغة موحدة، وCORS بقائمة محددة، وTLS في كل اتصال؟ (API8)
- هل ال Endpoint في ملف OpenAPI، وله مالك وجمهور، وخطة إيقاف إذا كان نسخة جديدة تحل محل قديمة؟ (API9)
- إذا كان يستدعي خدمة خارجية، هل لكل اتصال مهلة وحد للحجم، وهل يتحقق من الرد قبل استخدامه؟ (API10)
- هل كل رفض يكتب سطراً منظماً في السجل دون أسرار، ومع تنبيه يصل إلى شخص معروف؟

## مقالات مرتبطة

هذه المقالات تتعمق في أجزاء هذا المقال: القائمة العامة لأمن الويب، ومشاريع OWASP، والصلاحيات ومكانها، وعزل المستأجرين في قاعدة البيانات، والحماية على مستوى HTTP وحد المعدل، والفحص الديناميكي من ملف OpenAPI:

[![](https://arabroot.io/content/images/2026/10/owasp-top10-cover-cover.webp)دليلOWASP Top 10 للمبتدئين: أخطر عشر مشكلات في تطبيقات الويبأخطر عشر مشكلات أمنية في تطبيقات الويب، كل واحدة بمثال ضعيف ثم مصلح، وكيف تمنعها في الكود وأين تكتشفها.](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/owasp-overview-cover-cover.webp)دليلما هي OWASP؟ مشاريعها ووثائقها ومتى تستخدم كلاً منهامن يدير OWASP ولماذا كل ما تنشره مجاني، وأي مورد من مواردها تفتح في كل مرحلة من بناء تطبيقك.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-owasp-%D9%85%D8%B4%D8%A7%D8%B1%D9%8A%D8%B9%D9%87%D8%A7-%D9%88%D9%88%D8%AB%D8%A7%D8%A6%D9%82%D9%87%D8%A7-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/authorization-cover-cover.webp)دليلAuthorization: كيف تجمع قرارات الصلاحيات في مكان واحدلماذا تتحول شروط الصلاحيات المتفرقة في الكود إلى ثغرات، وكيف تنقل القرار إلى محرك سياسات واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/)[![](https://arabroot.io/content/images/2026/10/multi-tenancy-practice-cover-cover.webp)دليلتطبيق SaaS متعدد المستأجرين عملياً مع Row-Level Securityنبني على PostgreSQL تطبيق SaaS يعزل كل مستأجر Tenant عن غيره بسياسات Row-Level Security، ونرى أين تتسرب البيانات رغم ذلك.](https://arabroot.io/articles/%D8%A8%D9%86%D8%A7%D8%A1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-saas-%D9%85%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-%D8%B9%D9%85%D9%84%D9%8A%D8%A7/)[![](https://arabroot.io/content/images/2026/10/http-protection-cover-cover.webp)دليلحماية تطبيقات الويب بطبقات أمان في ال Reverse Proxyكلمة المرور وحدها لا تحمي تطبيقاً مكشوفاً على الإنترنت، لذلك نضع أمامه طبقات حماية في ال Reverse Proxy نفسه.](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D9%88%D9%8A%D8%A8-%D8%B9%D9%84%D9%89-%D9%85%D8%B3%D8%AA%D9%88%D9%89-http/)[![](https://arabroot.io/content/images/2026/10/dast-cover-cover.webp)دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.](https://arabroot.io/articles/%D9%81%D8%AD%D8%B5-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D8%AB%D9%86%D8%A7%D8%A1-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-dast-%D9%85%D8%B9-zap/)

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- OWASP API Security Top 10 وثيقة توعية، ونسختها الحالية 2023 هي الثانية بعد 2019، ولم تصدر نسخة أحدث حتى أكتوبر 2026، وقد بنيت من حوادث منشورة ورأي خبراء لأن جمع البيانات لم يعط ما يكفي.
- ثلاثة من الخمسة الأولى أسئلة صلاحيات: BOLA للكائن، وBOPLA للحقل، وBFLA للوظيفة، والواجهة لا تحمي أياً منها لأن ال API يقبل الطلب من أي كلاينت.
- في التجربة رأينا alice تقرأ طلب bob وتصدر كل المستخدمين بهاش كلمات مرورهم، وbob يجعل نفسه مديراً بحقل واحد، وطلباً واحداً يخرج 10 ميجابايت، والسيرفر يعيد أسرار خدمة داخلية من رابط، ثم رأينا كل ذلك يختفي بقوائم سماح صريحة للحقول والمضيفين وحد للصفحة وفحص للملكية والدور.
- اختبارات الصلاحيات الآلية التي فشلت على النسخة الضعيفة ونجحت على المصلحة هي أقوى أداة لمخاطر الصلاحيات، وتعمل في خط CI مثل أي اختبار.
- البوابة تحد الكثرة وتتحقق من التوكن، ولكنها لا ترى البيانات، فالقرار الدقيق يبقى في التطبيق، وملف OpenAPI هو العقد والسجل وأساس الفحص معاً.
- كل نسخة تعمل يجب أن تكون في السجل بمالك ومراقبة، وكل خدمة خارجية تعامل ردودها كمدخلات مستخدم مجهول.

وفي المقالات القادمة بإذن الله سوف نكمل مرحلة أمن التطبيقات بالبرمجة الآمنة بأمثلة في كل لغة، ثم بقائمة مخاطر تطبيقات نماذج اللغة LLM.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال على نسخة OWASP API Security Top 10 2023 من موقع المشروع ومستودعه على GitHub، والتحقق من أسماء المخاطر العشر ومنهجية البناء والتغييرات عن 2019 وعدم وجود نسخة أحدث، والتحقق من تراخيص Kong وApache APISIX وTraefik وإصداراتها، وتنفيذ التجربة بمكتبة Node.js 22.23.3 القياسية داخل Docker Engine 29.8.0، مع اختبارات node:test.