مشاركة الموارد عبر الأصول (CORS)
مشاركة الموارد عبر الأصول (Cross-Origin Resource Sharing أو CORS) هي الطريقة التي يخبر بها الخادمُ المتصفحَ أيَّ الأصول الأخرى يحق لها قراءة استجاباته. والأصل (origin) هو البروتوكول والمضيف والمنفذ في العنوان، وهي أجزاء فصلتها في درس عناوين URL ومعاملات المسار والاستعلام. وبموجب سياسة الأصل نفسه (Same-Origin Policy)، تستطيع شيفرة الصفحة على https://shop.example.com إرسال طلب إلى https://api.example.com، لكنها لا تستطيع قراءة الاستجابة ما لم تسمح بذلك واجهة API عبر ترويسات CORS.
مثال: تستدعي صفحة المتجر fetch('https://api.example.com/v1/cart') فيضيف المتصفح Origin: https://shop.example.com. فإذا حملت الاستجابة Access-Control-Allow-Origin: https://shop.example.com حصلت الصفحة على السلة، وإلا فلا تتلقى شيفرتها إلا خطأ شبكة عاماً، وتعرض وحدة التحكم (Console) خطأ CORS.
لماذا يهم
حين تكون الواجهة الأمامية وواجهة API على أصلين مختلفين، تعطّل ترويسة CORS مفقودة أو خاطئة ميزات حقيقية: لا تُحمَّل السلة، ولا يفعل زر الحفظ شيئاً، ولا تظهر رسالة الخطأ أبداً. وكثيراً ما تفشل بيئة واحدة فقط، لأن لكل بيئة نطاقاتها وإعدادات بوابتها.
وتحمي CORS البيانات أيضاً: فالخادم الذي يسمح لأي أصل بقراءة الاستجابات المرسلة مع ملفات تعريف الارتباط الخاصة بالمستخدم قد يتيح لمواقع أخرى يزورها المستخدم أن تقرأ تلك البيانات. وبالنسبة إلى المختبِر، تفسّر CORS كثيراً من بلاغات «يعمل في Postman ويفشل في المتصفح».
الأصول وسياسة الأصل نفسه
يتشارك عنوانان الأصل نفسه فقط إذا تطابق البروتوكول والمضيف والمنفذ جميعاً، ولا يهمّ المسار ولا الاستعلام. فالنطاق الفرعي مضيف مختلف، وhttp بروتوكول مختلف عن https، لذا فالمتجر وواجهته البرمجية أصلان مختلفان وإن كانت تديرهما شركة واحدة.
تتعلق سياسة الأصل نفسه بالقراءة أساساً لا بالإرسال: فالروابط والنماذج والصور والسكربتات القادمة من أصول أخرى تعمل كالمعتاد. ما تمنعه هو أن تقرأ شيفرة الصفحة استجابةً من أصل آخر. وتخفّف CORS هذه القاعدة بشروط الخادم: يرسل الخادم الترويسات ويطبّقها المتصفح، أما العملاء خارج المتصفح، مثل curl وPostman، فيتجاهلونها.
Page origin: https://shop.example.com
URL Same origin? Why
https://shop.example.com/cart?id=7 yes only path and query differ
https://shop.example.com:443/help yes 443 is the default https port
http://shop.example.com/cart no scheme differs
https://api.example.com/v1/cart no host differs (subdomain)
https://shop.example.com:8443/cart no port differsالطلبات البسيطة والطلبات التي يسبقها طلب تمهيدي
طلب GET أو HEAD أو POST لا يحمل إلا ترويسات من القائمة الآمنة في CORS، مثل Accept أو Content-Type بإحدى القيم text/plain أو multipart/form-data أو application/x-www-form-urlencoded، يُرسل فوراً ومعه ترويسة Origin. ويسمّيه MDN «الطلب البسيط» (Simple Request)، مع أن معيار Fetch لا يستخدم هذا الاسم. ويعالجه الخادم أياً كانت إعدادات CORS لديه؛ فـCORS لا تقرر إلا ما إذا كان يحق للصفحة قراءة الاستجابة.
وأي طلب آخر إلى أصل مختلف يسبقه طلب تمهيدي (Preflight): PUT وPATCH وDELETE، أو POST مع Content-Type: application/json، أو ترويسة مثل Authorization. يرسل المتصفح أولاً طلب OPTIONS يذكر فيه الطريقة والترويسات التي يريد استخدامها. ولا يُرسل الطلب الفعلي إلا إذا تلقى الطلب التمهيدي استجابة بحالة من فئة 2xx تسمح ترويساتها بالأصل والطريقة والترويسات، ثم تُفحص استجابته مرة أخرى. وإذا أُجيب الطلب التمهيدي بإعادة توجيه أو بحالة خطأ فإنه يفشل، ولا يغادر الطلب الفعلي المتصفح أبداً.
وبيانات الاعتماد (Credentials)، أي ملفات تعريف الارتباط أو مصادقة HTTP التي لا تُرسل إلى أصل آخر إلا إذا طلبتها الشيفرة بـcredentials: 'include' في fetch أو بـwithCredentials = true في XMLHttpRequest، تشدّد القواعد: يجب أن تذكر الاستجابتان الأصل بالضبط لا *، وأن ترسلا Access-Control-Allow-Credentials: true، ولا تعود * في قائمتي الطرق والترويسات المسموح بها حرفَ بدل (wildcard) أيضاً. أما الطلب التمهيدي نفسه فلا يحمل ملفات تعريف الارتباط أبداً.
طلب بسيط: بلا طلب تمهيدي
- صفحة المتجر
https://shop.example.com - المتصفح
- واجهة API
api.example.com
- 1 من صفحة المتجر إلى المتصفح: تطلب قراءة السلة
GET /v1/cart - 2 من المتصفح إلى واجهة API: يرسله فوراً مع
OriginOrigin: https://shop.example.com - 3 من واجهة API إلى المتصفح: تعالجه وتستجيب ردّ أياً كانت إعدادات CORS لديها
- 4 المتصفح داخليًا: يفحص
Access-Control-Allow-Originأصل الصفحة، أو*إن لم تُرسل ملفات تعريف الارتباط - 5 من المتصفح إلى صفحة المتجر: البيانات، أو خطأ شبكة عام ردّ
النتيجة: يعالج الخادم الطلب في الحالتين؛ ولا تقرر CORS إلا ما إذا كان يحق للصفحة قراءة الإجابة.
- صفحة المتجر
طلب يسبقه طلب تمهيدي: PUT مع ملفات تعريف الارتباط
- صفحة المتجر
https://shop.example.com - المتصفح
- واجهة API
api.example.com
- 1 من صفحة المتجر إلى المتصفح: تطلب تحديث السلة جسم JSON و
credentials: 'include'PUT /v1/cart/items/42 - 2 من المتصفح إلى واجهة API: الطلب التمهيدي، بلا ملفات تعريف الارتباط يذكر الطريقة والترويسات التي يريد استخدامها
OPTIONS /v1/cart/items/42 - 3 من واجهة API إلى المتصفح: حالة 2xx مع
Access-Control-Allow-*ردّ - 4 المتصفح داخليًا: يفحص الأصل والطريقة والترويسات وبيانات الاعتماد إعادة التوجيه أو حالة الخطأ أو أي عدم تطابق يوقف كل شيء هنا
- 5 من المتصفح إلى واجهة API: الطلب الفعلي، مع ملفات تعريف الارتباط
PUT /v1/cart/items/42 - 6 من واجهة API إلى المتصفح: استجابة تذكر الأصل بالضبط ردّ ومعها
Access-Control-Allow-Credentials: true - 7 المتصفح داخليًا: يفحص الاستجابة مرة أخرى
- 8 من المتصفح إلى صفحة المتجر: البيانات، أو خطأ شبكة عام ردّ
النتيجة: إذا فشل فحص الطلب التمهيدي فلن يُرسل
PUTأبداً.- صفحة المتجر
ما دامت نتيجة الطلب التمهيدي المخزنة مؤقتاً صالحة (Access-Control-Max-Age)، يتخطى المتصفح الخطوات من 2 إلى 4 في اللوحة الثانية.
Origin(يضعها المتصفح)، ومعها في الطلب التمهيديAccess-Control-Request-MethodوAccess-Control-Request-Headers: من يطلب، وما الذي سيستخدمه الطلب الفعلي.Access-Control-Allow-Origin: الأصل الوحيد المسموح له بالقراءة، أو*حين لا تدخل بيانات اعتماد.Access-Control-Allow-MethodsوAccess-Control-Allow-Headers: ما تسمح به استجابة الطلب التمهيدي.Access-Control-Allow-Credentials: true: مطلوبة عند إرسال بيانات الاعتماد.Access-Control-Expose-Headers: ترويسات استجابة إضافية يحق للشيفرة قراءتها، مثلX-Total-Count. وAccess-Control-Max-Age: عدد الثواني التي يجوز فيها إعادة استخدام نتيجة الطلب التمهيدي، ضمن الحد الذي يفرضه كل متصفح.Vary: Origin: تخبر ذاكرات التخزين المؤقت بأن الاستجابة تختلف باختلاف الأصل، وهي لازمة حين يعيد الخادم الأصل في استجابته.
أخطاء الإعداد الشائعة
الإعدادات المفرطة في التشدد أو غير المتسقة تحجب الواجهة الأمامية الحقيقية وتعطّل الميزات، والإعدادات المفرطة في التساهل تتيح لمواقع أخرى قراءة البيانات عبر متصفح مستخدم مسجّل الدخول، ولهذا يتناول دليل OWASP لاختبار أمان الويب (Web Security Testing Guide) إعدادات CORS. ويتوقف مدى ما يبلغه موقع آخر فعلاً على سمة SameSite في ملفات تعريف الارتباط وعلى إعدادات المتصفح لملفات تعريف الارتباط التابعة لجهات خارجية (انظر درس ملفات تعريف الارتباط والجلسات)، لكن المختبِر يُبلغ عن السياسة المفرطة في التساهل نفسها.
- يعطّل الميزات: تطبيق المصادقة على
OPTIONS، فيتلقى الطلب التمهيدي الخالي من ملفات تعريف الارتباط الحالة401أو403. - يعطّل الميزات: إعادة توجيه على الطلب التمهيدي، مثل إضافة شرطة مائلة في نهاية المسار (انظر درس إعادة التوجيه في HTTP).
- يعطّل الميزات: ترويسات CORS على استجابات النجاح فقط، فتصل الحالة
400أو500إلى الصفحة خطأَ شبكة عاماً. - يعطّل الميزات: إرسال
Access-Control-Allow-Originمرتين (من التطبيق ومن البوابة) أو في صورة قائمة؛ ويجب أن تحمل قيمة واحدة. - يكشف البيانات: إعادة أي
OriginمعAccess-Control-Allow-Credentials: true، أو السماح بـOrigin: nullالتي يستطيع أي موقع إنتاجها من إطار معزول (sandboxed). - يكشف البيانات: مطابقة الأصول بالبادئة أو اللاحقة أو بنمط غير مثبّت الطرفين، فيمرّ
https://shop.example.com.attacker.testأوhttps://notexample.com.
كيف تختبر CORS
ابدأ في متصفح حقيقي، لأن المتصفح وحده هو من يطبّق CORS. في لوحة Network ضمن أدوات المطوّرين يظهر الطلب التمهيدي سطراً مستقلاً بالطريقة OPTIONS قبل الطلب الفعلي مباشرةً (وإن لم يظهر فأزل مرشّح Fetch/XHR). وتشرح وحدة التحكم سبب حظر الاستجابة، بينما لا تتلقى شيفرة الصفحة إلا خطأً عاماً، وهو في Chrome TypeError: Failed to fetch.
ثم أعد إرسال الطلبات باستخدام curl أو أداة CORS Tester، وضع أي قيمة تحتاجها في Origin، وطبّق فحوص المتصفح بنفسك، على الأنظمة التي تملكها أو لديك إذن مكتوب باختبارها فقط. واستخرج شروط الاختبار من المستدعين الحقيقيين: كل أصل للواجهة الأمامية في كل بيئة، وكل طريقة وترويسة، مع ملفات تعريف الارتباط ومن دونها.
- الأصل المسموح: طلب تمهيدي بحالة 2xx وترويسات
Allow-*الصحيحة، وAccess-Control-Allow-Originفي الاستجابة الفعلية أيضاً. - أصول غير مدرجة مثل
https://attacker.testوnull، وصيغ معدّلة من أصل مسموح:http://بدلاً منhttps://، ومنفذ آخر، ومضيفون متشابهون في الاسم مثلhttps://shop.example.com.attacker.test. ولا يجوز أن يُعاد أي منها. - كل طريقة وترويسة ترسلها الواجهة الأمامية فعلاً، لا
GETوحدها. - استجابات الخطأ (
401و404و500) بترويسات CORS نفسها التي في استجابات النجاح. - مع بيانات الاعتماد: لا
*، وVary: Originكلما أُعيد الأصل.
curl -i -X OPTIONS https://api.example.com/v1/cart/items/42 \
-H "Origin: https://shop.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type"جرّبها: فاحص CORS جرّبها: مولّد أوامر cURL جرّبها: محلّل ترويسات HTTP
مثال تطبيقي: تحديث السلة محظور في بيئة الإنتاج
في بيئة Staging يعمل تغيير الكمية في السلة، أما في بيئة الإنتاج فلا يحدث شيء وتعرض وحدة التحكم خطأ CORS. ترسل الواجهة الأمامية على https://shop.example.com الطلب PUT /v1/cart/items/42 إلى https://api.example.com مع جسم JSON وملفات تعريف الارتباط، فيسبقه المتصفح بطلب تمهيدي. ويعيد المختبِر إرسال الطلب التمهيدي باستخدام curl.
الحالة والأصل وبيانات الاعتماد صحيحة، لكن Access-Control-Allow-Methods تخلو من PUT، فيتوقف المتصفح بعد الطلب التمهيدي ولا يصل PUT إلى واجهة API أبداً، ولا يُظهر أي سجل في الخادم تحديثاً فاشلاً. وتعمل بيئة Staging لأن بوابتها مُعدّة على حدة.
وعند تكرار الطلب التمهيدي وطلب GET /v1/cart مع Origin: https://attacker.test ومع أصل آخر لا علاقة له بالتطبيق، يُعاد الأصلان كلاهما مع Access-Control-Allow-Credentials: true: عيب أمني مستقل يُبلَّغ عنه عبر قناة خاصة، لأن مواقع أخرى تستطيع حينئذٍ قراءة بيانات العميل المسجّل الدخول.
OPTIONS /v1/cart/items/42 HTTP/1.1
Host: api.example.com
Origin: https://shop.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Vary: Originأخطاء شائعة
- افتراض أن خطأ CORS يعني أن الطلب لم يصل إلى الخادم: فالطلب البسيط أُرسل وعولج، وربما غيّر طلب
POSTالبيانات فعلاً، أما بعد فشل الطلب التمهيدي فلم يُرسل الطلب الفعلي أصلاً. - عدّ الإصلاح متحققاً منه لأن curl أو Postman صار يعمل. فهما يتجاهلان CORS، والفحص الأخير مكانه المتصفح.
- اختبار الأصل المسموح فقط، وعدم تجربة أصل غير مدرج أو متشابه في الاسم أو
null. - اختبار
GETومسار النجاح فقط، فيمرّ غيابPUTأو غياب الترويسات في استجابات500دون أن يلاحظه أحد. - طلب
Access-Control-Allow-Origin: *لتجاوز الحظر أثناء الاختبار: فهي تفشل مع ملفات تعريف الارتباط وتفتح واجهة API لكل المواقع. - إعادة اختبار قاعدة شُدّدت قبل الأوان: قد يعيد المتصفح استخدام نتيجة تمهيدية مخزنة أكثر تساهلاً حتى تنتهي مدة
Access-Control-Max-Age.
من منظور ضمان الجودة
في الفريق، مكان CORS هو مراجعات واجهات API وقوائم فحص البيئات. اسأل مبكراً: أي الأصول تستدعي الواجهة في كل بيئة؟ هل يرسل أي استدعاء ملفات تعريف الارتباط؟ ما الطرق والترويسات المخصصة التي تستخدمها الواجهة الأمامية؟ ومن يملك الإعداد: التطبيق أم البوابة أم شبكة توصيل المحتوى (CDN)؟
عند الإبلاغ عن عيب في CORS، أرفق الطلب التمهيدي والطلب الفعلي بترويساتهما، وقيمة Origin بالضبط، والمتصفح، ورسالة وحدة التحكم، واذكر القيمة المتوقعة («يجب أن تتضمن Access-Control-Allow-Methods الطريقة PUT»). وأبلغ عن السياسة المفرطة في التساهل بوصفها ملاحظة أمنية عبر قناة خاصة، وأبقِ فحص الأصل غير المدرج في حزمة الانحدار بعد الإصلاح.
تمرين عملي
راجع ثلاث استجابات لطلبات تمهيدية
واجهة أمامية على https://portal.example.com تستدعي DELETE /v1/sessions/current على https://api.example.com مع ملفات تعريف الارتباط (credentials: 'include') والترويسة X-CSRF-Token. فيما يلي إجابات واجهة API عن ثلاثة طلبات تمهيدية لهذا الاستدعاء. قرّر لكل منها هل يرسل المتصفح DELETE، واذكر كل مشكلة.
# A request Origin: https://portal.example.com
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-CSRF-Token
Access-Control-Allow-Credentials: true
# B request Origin: https://portal.example.com
HTTP/1.1 401 Unauthorized
Access-Control-Allow-Origin: https://portal.example.com
Access-Control-Allow-Credentials: true
# C request Origin: https://evil.test
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://evil.test
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: X-CSRF-Token
Access-Control-Allow-Credentials: true
Vary: Origin- الحالة: هل هي من فئة 2xx؟
- قارن
Access-Control-Allow-Originبأصل الطلب وفق قاعدة بيانات الاعتماد. - هل
DELETEوX-CSRF-Tokenمسموح بهما؟ - هل يُرسل
DELETE؟ صنّف كل مشكلة: وظيفية أم أمنية.
إظهار الإجابة النموذجية
A: محظور، وهو عيب وظيفي. يرسل الاستدعاء ملفات تعريف الارتباط، ومع بيانات الاعتماد يجب أن تكون قيمة Access-Control-Allow-Origin هي الأصل بالضبط لا *. الإصلاح: إعادة https://portal.example.com، مع Vary: Origin إن كانت القيمة تُختار لكل طلب.
B: محظور، وهو عيب وظيفي. يحتاج الطلب التمهيدي إلى حالة من فئة 2xx، والحالة 401 تدل على أن المصادقة تعمل قبل الرد على OPTIONS، مع أن الطلب التمهيدي لا يحمل ملفات تعريف الارتباط أبداً. كما أن DELETE وX-CSRF-Token غير مسموح بهما. الإصلاح: الرد على الطلبات التمهيدية قبل المصادقة، مع ترويسات Allow-* كاملة.
C: يسمح المتصفح بمروره، وهنا المشكلة: فواجهة API تقبل مع بيانات الاعتماد أصلاً ليس من واجهاتها الأمامية، لذا تستطيع صفحة على https://evil.test إرسال طلبات مع ملفات تعريف الارتباط الخاصة بمستخدم مسجّل الدخول وقراءة استجاباتها. إنه عيب أمني؛ تأكد منه بأصل آخر لا علاقة له بالتطبيق، وأبلغ عنه عبر قناة خاصة.
أسئلة شائعة
هل تحمي CORS واجهتي البرمجية من المهاجمين؟
لا. تتحكم CORS فيما يسمح المتصفح لصفحة الويب بقراءته، ولا تتحقق من هوية أحد. فـcurl والسكربتات والخوادم الأخرى تستطيع استدعاء واجهة API بأي ترويسة Origin تشاء، لذا تظل الواجهة بحاجة إلى المصادقة والتفويض. وليست CORS دفاعاً ضد CSRF أيضاً: فالطلبات البسيطة تصل إلى الخادم قبل أي فحص لـCORS.
لماذا ينجح الطلب في Postman ويفشل في المتصفح؟
Postman وcurl ليسا متصفحين ولا يطبّقان سياسة الأصل نفسه، فلا يرسلان طلباً تمهيدياً ولا يفحصان ترويسات Access-Control-*. أما المتصفح فيفعل الأمرين. افحص الطلب التمهيدي الذي أرسله المتصفح في لوحة Network، وقارنه بما يعيده الخادم فعلاً لطلب OPTIONS يحمل قيمة Origin نفسها.
هل Access-Control-Allow-Origin: * خطأ دائماً؟
لا. للبيانات العامة التي لا تحتاج إلى ملفات تعريف الارتباط، مثل كتالوج عام، تكون * مقبولة وبسيطة. لكنها خاطئة للاستجابات التي تعتمد على ملفات تعريف الارتباط الخاصة بالمستخدم (والمتصفحات ترفض * مع بيانات الاعتماد أصلاً)، وخطرة على واجهات API الداخلية التي تثق بشبكتها بدلاً من المصادقة على المستدعين.
المراجع
- Fetch Standardيعرّف CORS: طلبات التحقق المسبق وترويسات Access-Control-* والاستجابات التي يسمح المتصفح للصفحة برؤيتها.
- Cross-Origin Resource Sharing (CORS) — MDNكيف يعمل CORS عملياً: الطلبات البسيطة مقابل الطلبات ذات التحقق المسبق، وبيانات الاعتماد، والأخطاء التي تبلّغ عنها المتصفحات.
- OWASP Web Security Testing Guide (WSTG)المنهجية المرجعية لاختبار تطبيقات الويب وواجهات API بحثاً عن نقاط الضعف الأمنية، اختباراً تلو الآخر.
- HTML Living Standardلغة HTML، بما فيها مراجع المحارف المسماة (الكيانات) وقواعد التحليل التي تتبعها المتصفحات.
تدرّب بأدوات متقن
اختبار إعدادات CORS: إرسال طلبات تمهيدية (preflight) وطلبات بسيطة من أصل محدّد وفحص ترويسات Access-Control-*.
إنشاء أوامر cURL من الطريقة وعنوان URL والترويسات والمتن وخيارات المصادقة.
تحليل ترويسات طلبات واستجابات HTTP الخام إلى جدول منظّم مع الشروحات.
تكوين ترويسات HTTP باستخدام إعدادات مسبقة للتخزين المؤقت وCORS والأمان والتفاوض على المحتوى.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.