ملخص: يُعد مُدخل واجهة برمجة التطبيقات (API) الخاصة بك سطح هجوم، لذا اختبره على هذا الأساس. اكتب حالات اختبار سلبية ترسل حقولًا بحجم مفرط، وأنواعًا خاطئة، وأجسامًا مشوهة، وسلاسل حقن (injection strings)، ثم تأكد من أن نقطة النهاية تستجيب برمز 4xx وليس 5xx أبدًا. حوّل التحقق من المخطط (schema validation) إلى إجراء أمني باستخدام `additionalProperties: false`، والتعدادات (enums)، وحدود الطول. شغل مجموعة الاختبارات الكاملة في CI (الدمج المستمر) مع كل تغيير. جعلت وكلاء الذكاء الاصطناعي هذا الأمر ملحًا: فهم ينشئون ويُعيدون توجيه الحمولة (payloads) بسرعة آلية، لذا فإن "تحميل هذه البيانات" الذي يتحول بهدوء إلى "تشغيل هذا الكود" يتسع الآن.
معظم مجموعات الاختبار تثبت أن واجهة برمجة التطبيقات (API) الخاصة بك تعمل عندما يكون المتصل مهذبًا. ترسل جسم طلب صالحًا، فتحصل على 200، ويمر التأكيد. هذه النتيجة لا تخبرك بشيء تقريبًا عما يحدث عندما يكون جسم الطلب عدائيًا. المدخلات غير الموثوق بها هي أي بيانات لم تُنشئها نقطة النهاية الخاصة بك بنفسها: نصوص الطلبات، سلاسل الاستعلام، الرؤوس، تحميلات الملفات، حمولات الويب هوك (webhook payloads)، وبيانات JSON التي يقوم وكيل الذكاء الاصطناعي بتجميعها في الحال. كل هذا يستحق نفس الافتراض، وهو أن شخصًا ما سيرسل في النهاية أسوأ نسخة ممكنة منه.
في يوليو 2026، وصفت Hugging Face حادثًا أمنيًا كان متجه الدخول فيه هو البيانات، وليس كلمة مرور مسروقة. لقد غطينا الدروس المستفادة من هذا الاختراق بشكل منفصل؛ وهذا الدليل هو الجزء العملي. ستقوم ببناء اختبارات ترسل نوع المدخلات التي يرسلها المهاجم، ثم تشغلها تلقائيًا مع كل تغيير. تتوافق الفئات مع قائمة OWASP لأهم 10 مخاطر أمنية لواجهات برمجة التطبيقات، والتي تستحق الاحتفاظ بها مفتوحة في علامة تبويب. يُعد Apidog إحدى الطرق لتصميم العقد وتشغيل هذه الاختبارات، لكن الأفكار تنطبق على أي إطار عمل تستخدمه بالفعل.
المدخلات هي سطح هجوم، وليست حقل نموذج
غالبًا ما يُنظر إلى التحقق من الصحة (Validation) على أنه مجرد تهذيب لتجربة المستخدم: اكتشف البريد الإلكتروني الفارغ، أظهر إطارًا أحمر، وامض قدمًا. هذا التصور هو المشكلة. كل حقل تقبله واجهة برمجة التطبيقات (API) الخاصة بك هو وعد يمكن للمتصل أن يخالفه، وكل وعد مكسور هو مسار إلى منطقك. معلمة `limit` التي توقعت أن تكون عددًا صحيحًا صغيرًا تصبح `999999999`. اسم ملف `filename` الذي توقعت أن يكون كلمة واحدة يصبح `../../etc/passwd`. كائن `config` الذي توقعت أن يحمل الإعدادات يصبح مجموعة من التعليمات.
اختبار الأمان ليس تخصصًا منفصلًا يُضاف في النهاية. إنه نفس الاختبار السلبي الذي تعرفه بالفعل، ويستهدف الحقول الأكثر احتمالًا لإلحاق الضرر بك. إذا بنيت عادة السؤال "ما هو أسوأ شيء يمكن أن يناسب هذا الحقل؟"، فأنت في معظم الطريق نحو الممارسات الموجودة في دليلنا لأفضل ممارسات أمان واجهة برمجة التطبيقات (API). يحول بقية هذا المقال هذا السؤال الواحد إلى اختبارات ملموسة يمكنك تشغيلها.
كيف تحولت "حمّل هذه البيانات" إلى "شغل هذا الكود"
حادثة Hugging Face هي مثال واضح على سبب استحقاق المدخلات لهذا الاهتمام. قالت Hugging Face إن متجه الدخول كان مجموعات بيانات خبيثة: مجموعة بيانات مُعدّة بعناية أدت إلى تشغيل مُحمِّل بيانات يقوم بتنفيذ كود عن بعد (remote-code dataset loader)، وكان هناك حقن قالب (template injection) ضمن إعدادات مجموعة البيانات. يمكنك قراءة تقرير الشركة الخاص في تقرير الحادث الأمني الخاص بها.
تأمل شكل هذا الفشل. قبلت نقطة نهاية شيئًا وُصف بأنه بيانات. أدى تحميل تلك البيانات إلى تشغيل مسار كود يمكنه تنفيذ تعليمات يتحكم فيها المهاجم. تحولت "حمّل هذه البيانات" إلى "شغل هذا الكود". حقن القالب هو نفس القصة على نطاق أصغر: قيمة تكوين كان من المفترض أن تكون نصًا خاملًا تم تقييمها، لذا أصبح النص تنفيذًا.
الخلاصة ليست أن "Hugging Face ارتكبت خطأ نادرًا". بل هي أن أي نقطة نهاية تقبل اسم مُحمِّل (loader name)، أو تنسيقًا، أو قالبًا، أو كائنًا مُسلسلاً، أو كتلة إعدادات (config blob) هي في الواقع تقبل تعليمات، سواء قصدت ذلك أم لا. إذا لم تكتب أبدًا اختبارًا يرسل إعدادًا عدائيًا إلى نقطة النهاية تلك، فأنت لم تتحقق فعليًا من الافتراض بأنها تظل خاملة. وهذا الافتراض غير المختبر هو نقطة الضعف بأكملها.
التحقق من المخطط كإجراء أمني
أرخص إجراء تحكم يمكنك إضافته هو مخطط صارم عند الحافة. المخطط ليس مجرد وثائق. عندما ترفض أي شيء لا يتطابق، يصبح المخطط مرشحًا يعمل قبل أن يرى منطق عملك الطلب على الإطلاق. يمنحك JSON Schema الأدوات الأساسية لجعل هذا المرشح محكمًا.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["loader", "name"],
"properties": {
"loader": { "enum": ["csv", "json", "parquet"] },
"name": { "type": "string", "maxLength": 128, "pattern": "^[\\w .-]+$" },
"rows": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
اقرأها على أنها أربعة دفاعات منفصلة. `additionalProperties: false` يرفض حقلاً مُهرَّبًا `template` بشكل مباشر، بحيث لا يمكن للمهاجم إضافته. يعني التعداد (enum) الخاص بـ `loader` أن `pickle://` أو أي محمل كود عن بُعد ليس قيمة صالحة ببساطة. `maxLength` يقضي على السلسلة النصية ذات الحجم الكبير التي تهدف إلى استنزاف الذاكرة. `pattern` الموجود على `name` يرفض الأحرف `{{` و `'; DROP TABLE` قبل أن تنتقل أبعد من ذلك. لا تعرف أي من هذه الأسطر شيئًا عن المهاجمين. إنها تقبل فقط المجموعة الضيقة من المدخلات التي تدعمها بالفعل، وهذا التضييق هو خاصية الأمان.
التحقق من العقد بهذه الطريقة لا يكتشف كل الثغرات، ولن يفعل أي مخطط ذلك. ما يغلقه هو فئة محددة وشائعة: خطأ "لم نتحقق أبدًا مما تقبله نقطة النهاية هذه". هذه الفئة هي المكان الذي يبدأ فيه عدد مدهش من الاختراقات.
الاختبار السلبي: أثبت أن نقطة النهاية ترفض
تؤكد اختبارات المسار السعيد أن المدخلات الجيدة تنتج مخرجات جيدة. تؤكد الاختبارات السلبية أن المدخلات السيئة تنتج رفضًا متحكمًا فيه. يهم هذا التمييز لأن الرفض يعتبر ميزة: رمز 400 مع خطأ واضح يعني أن واجهة برمجة التطبيقات (API) الخاصة بك تدافع عن حدودها. أما رمز 500 فيعني أن واجهة برمجة التطبيقات الخاصة بك تفقد السيطرة عليها.
ابنِ حالات الاختبار السلبية بنفس الطريقة في كل مرة. لكل حقل، اكتب ما يجب أن يرفضه: نوع خاطئ، مفقود عندما يكون مطلوبًا، موجود عندما يكون محظورًا، طويل جدًا، خارج النطاق، وسلاسل الحقن (injection strings) التي تناسب تنسيقه. ثم تأكد من أمرين في الاستجابة. أولاً، أن يكون رمز الحالة 4xx، وعادة ما يكون 400 أو 422. ثانيًا، ألا يكون رمز الحالة 5xx أبدًا. رمز 500 يعني أن مدخلاتك العدائية وصلت إلى كود لم يكن جاهزًا لها، وهذا هو بالضبط الوصول الذي يريده المهاجم. تحتوي قائمة التحقق من اختبار أمان واجهة برمجة التطبيقات (API) الخاصة بنا على قائمة أولية لكل حقل يمكنك تكييفها.
هناك قاعدة واحدة تحافظ على نزاهة هذا الأمر: تأكد من السلوك، وليس من نص الخطأ. إذا تأكدت من أن الرسالة تقرأ "invalid loader" (محمل غير صالح)، فإن إعادة هيكلة غير ضارة ستكسر اختبارك وتعلم الفريق تخفيفه. تأكد من رمز الحالة، وحيثما أمكن، تأكد من عدم حدوث أي آثار جانبية على الإطلاق.
فئات الحقن التي تستحق اختبارًا مخصصًا
تظهر بعض فئات الحقن (injection families) بشكل متكرر بما يكفي لتستحق كل منها حالات اختبار دائمة، وليست مجرد فحص يدوي لمرة واحدة. لست بحاجة إلى أن تكون شاملًا هنا. تحتاج إلى حالة اختبار استكشافية واحدة لكل فئة لكي يفشل أي تراجع (regression) بصوت عالٍ. يمكن للأدوات التي تُشغل الكشف الآلي عن ثغرات واجهة برمجة التطبيقات (API) توسيع التغطية لاحقًا، ولكن عدد قليل من الحالات المكتوبة يدويًا يكتشف الثغرات الواضحة أولاً.
- **حقن SQL (SQL injection):** أرسل `1); DROP TABLE datasets;--` إلى أي حقل يصل إلى استعلام. يجب أن تتعامل نقطة النهاية معه كقيمة حرفية وتجيب برمز 400، أو تعيد نتيجة فارغة، وألا تظهر أبدًا خطأ قاعدة بيانات.
- **حقن القوالب (Template injection):** أرسل `{{ 7*7 }}` و `{{ config.__class__ }}` إلى حقول الاسم والتسمية. إذا احتوت الاستجابة على `49` في أي وقت، فهذا يعني أن محرك قوالب قام بتقييم مدخلاتك، وهذا كود عن بعد ينتظر أن يُنفذ.
- **إلغاء التسلسل غير الآمن ومُحمِّلات الكود عن بعد (Insecure deserialization and remote-code loaders):** أرسل `loader` بقيمة `pickle://` أو كائنًا مُسلسلاً حيث ينتمي إليه قيمة عادية. هذا هو شكل حادثة Hugging Face تمامًا. يجب أن ترفض نقطة النهاية المُحمِّلات غير المعروفة بواسطة قائمة السماح (allowlist)، بدلاً من محاولة أن تكون مفيدة.
- **حقن الأوامر (Command injection):** أرسل `; id` و `$(id)` إلى أي حقل قد يصبح وسيطًا لصدفة (shell argument)، مثل اسم ملف أو خيار تحويل. استجابة 200 التي تسرب معرّف مستخدم هي اكتشاف حاسم، وليست مجرد فضول.
الحجم الزائد، الشكل الخاطئ، والخلط في نوع المحتوى
ليس كل مدخل عدائي عبارة عن سلسلة نصية ذكية. بعضها ببساطة كبير جدًا أو مشوه الشكل، وغالبًا ما تؤدي هذه المدخلات إلى تعطيل المحللات (parsers) قبل أن يبدأ منطق التحقق الخاص بك بالعمل.
أرسل حمولة ذات حجم مفرط: حقل واحد يحتوي على خمسة ميغابايت من حرف واحد، أو مصفوفة JSON تحتوي على مليون عنصر. تفرض واجهة برمجة تطبيقات (API) صحية حدًا لحجم النص وتُعيد رمز 413 بدلاً من تخصيص الذاكرة حتى تنهار. أرسل نصوصًا مشوهة أيضًا: JSON مقطوع، فاصلة زائدة في النهاية، أو JSON متداخلًا ألف مستوى عميقًا لاختبار استنزاف الستاك (stack exhaustion). الإجابة الصحيحة هي 400 سريع، وليس عاملًا معلقًا.
الخلط في نوع المحتوى (Content-type confusion) هو المشكلة الصامتة. صرّح بـ `Content-Type: application/json` ولكن أرسل XML، أو صرّح بـ `application/xml` وأرسل حمولة مع كيان خارجي لاختبار XXE. اقلب الأمر وأرسل JSON كـ `text/plain` لترى ما إذا كان المحلل اللغوي المتساهل (lax parser) يقبله على أي حال. كل عدم تطابق يختبر ما إذا كان خادمك يثق بالرأس (header)، أو يثق بالنص (body)، أو يتحقق من تطابق الاثنين. يجب أن يطلب الخادم هذا التطابق قبل أن يحلل أي شيء.
لماذا تزيد وكلاء الذكاء الاصطناعي من المخاطر
كل ما سبق كان صحيحًا قبل وجود الوكلاء (agents). الوكلاء يغيرون الحجم والسرعة. المهاجم البشري يكتب طلبًا عدائيًا واحدًا في كل مرة. وكيل الذكاء الاصطناعي ينشئ ويعيد توجيه الحمولات بسرعة آلية، وسيقوم بسعادة ببناء مدخلات لن يكلف شخص نفسه عناء محاولتها.
ثلاث خصائص تجعل هذا الأمر أسوأ. يقوم الوكلاء بتركيب المدخلات، لذا فهم ينتجون قيم حقول لم يكتبها أي إنسان ولم يتوقعها أي اختبار. يعيد الوكلاء المحاولة ويُسلسلون المكالمات، لذا يمكن أن تتحول وثيقة واحدة سامة في المنبع إلى آلاف الطلبات العدائية ضد نقطة النهاية الخاصة بك في ثوانٍ. ويقوم الوكلاء بإعادة توجيه البيانات التي قيل لهم إنها موثوقة، وهذا هو كيف تصبح الحمولة المخفية في مجموعة بيانات أو ويب هوك (webhook) طلبًا حقيقيًا لواجهة برمجة التطبيقات (API) الخاصة بك. نمط Hugging Face، حيث يتحول "تحميل هذه البيانات" إلى "تشغيل هذا الكود"، هو بالضبط نوع التعليمات التي سينقلها الوكيل عبر حدود الثقة دون أن يلاحظ. تتعمق ملاحظتنا حول حقن المطالبات لفرق API في هذه العملية. لا يتغير الدفاع؛ لكنه يجب أن يكون تلقائيًا، لأنه لا يمكنك مراجعة حركة مرور الوكيل يدويًا.
ابنِ مجموعة الاختبارات السلبية وشغّلها في CI مع كل تغيير
حوّل الحالات المذكورة أعلاه إلى مجموعة اختبارات تعمل مع كل طلب سحب (pull request). إليك نسخة مدمجة ومُعَلّمة في pytest تستهدف نقطة نهاية اختبار (staging endpoint) وتؤكد على الرفض المتحكم فيه:
import httpx
import pytest
BASE = "https://staging.internal/v1"
HOSTILE_CONFIGS = [
{"loader": "pickle://s3/models/payload.pkl", "format": "auto"}, # remote-code loader
{"loader": "csv", "name": "{{ 7*7 }}"}, # template injection
{"loader": "csv", "name": "{{ config.__class__ }}"}, # object traversal
{"loader": "csv", "filter": "1); DROP TABLE datasets;--"}, # SQL injection
{"loader": "csv", "name": "A" * 5_000_000}, # oversized field
]
@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
assert r.status_code in (400, 413, 422), r.text # a boundary that says no
assert r.status_code < 500, "5xx means the payload reached logic it should not"
assert "49" not in r.text, "template rendered: server-side template injection"
ادمجها في CI (الدمج المستمر) لكي تمنع عمليات الدمج. وظيفة GitHub Actions بسيطة تقوم بالمهمة:
name: api-abuse-tests
on: [push, pull_request]
jobs:
negative-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/negative_input.py -q
هنا يكمن مكان أداة تعتمد على المخطط (schema-first tool). في Apidog، تقوم بتصميم نقطة النهاية وفقًا لعقد OpenAPI، بحيث يتم التحقق من كل طلب واستجابة مقابل هذا العقد أثناء الاختبار. يمكنك حفظ السيناريوهات السلبية جنبًا إلى جنب مع سيناريوهات المسار السعيد: الحقول ذات الحجم المفرط، والأنواع الخاطئة، وسلاسل الحقن المذكورة أعلاه، كل منها مع تأكيد بأن الحالة هي 4xx. ثم تقوم بتشغيل نفس السيناريوهات في CI (الدمج المستمر) عبر واجهة سطر الأوامر (CLI) الخاصة بـ Apidog، بحيث يؤدي أي تغيير يخفف من التحقق بهدوء إلى فشل البناء بدلاً من نشره. إذا كنت ترغب في تجربتها، قم بتنزيل Apidog وأضف سيناريو سلبيًا واحدًا إلى نقطة نهاية لديك بالفعل.
كن واضحًا بشأن الحدود. Apidog هي أداة تصميم واختبار ومحاكاة وتوثيق. إنها لا تشغل جدار حماية لتطبيقات الويب (WAF)، ولا تقوم بتصفية حركة المرور الحية، ولا تحل محل نظام إدارة معلومات الأمن والأحداث (SIEM)، ولن يكتشف التحقق من العقد أثناء الاختبار كل الثغرات. ما تفعله جيدًا هو جعل العقد صريحًا وتبقيك صادقًا بشأن ما تقبله نقطة النهاية، بحيث تتوقف فئة "لم نتحقق أبدًا" عن أن تكون الشيء الذي يفاجئك في الإنتاج.
أسئلة مكررة
- **ما الفرق بين الاختبار السلبي (negative testing) والاختبار العشوائي (fuzzing)؟** يرسل الاختبار السلبي مجموعة من المدخلات السيئة المنسقة التي اخترتها عمدًا، واحدة لكل فشل تهتم به. يرسل الاختبار العشوائي كميات كبيرة من المدخلات العشوائية أو المتحولة للعثور على حالات لم تفكر فيها. ابدأ بالاختبارات السلبية لأنها سريعة وحتمية وسهلة التشغيل في CI. أضف الاختبار العشوائي عندما تريد تغطية أوسع تتجاوز خيالك.
- **هل يجب تشغيل هذه الاختبارات على بيئة الإنتاج؟** لا. شغّلها على بيئة اختبار (staging) أو بيئة معزولة. بعض الحالات، مثل الحمولة كبيرة الحجم أو اختبار حقن الأوامر، مصممة لإجهاد النظام، وقد تؤدي بعضها إلى تغيير البيانات إذا كان هناك خطأ. تتيح بيئة الاختبار المخصصة أن تكون الاختبارات عدوانية دون أي مخاطر على المستخدمين الحقيقيين.
- **ألن يكتشف جدار الحماية أو جدار حماية تطبيقات الويب (WAF) هذا على أي حال؟** جدار حماية تطبيقات الويب (WAF) هو دفاع مفيد ومتعدد الطبقات، لكنه ليس بديلاً عن رفض التطبيق للمدخلات السيئة. يمكن تجاوز القواعد، ولا يمكن لجدار حماية تطبيقات الويب معرفة منطق عملك. الهدف من هذه الاختبارات هو إثبات أن نقطة النهاية نفسها ترفض، لذا فأنت لا تعتمد على مرشح لا تتحكم فيه بالكامل.
- **كم عدد الحالات السلبية الكافية لكل نقطة نهاية؟** استهدف حالة واحدة لكل حقل لكل فئة فشل يمكن أن يعاني منها: نوع خاطئ، خارج النطاق، طويل جدًا، حقل محظور، وأي سلسلة حقن (injection string) تناسب تنسيقه. عادة ما يكون هذا عددًا قليلاً من الحالات لكل نقطة نهاية، وليس المئات. تغطية الفئات أهم من العدد الخام.
- **هل يوقف التحقق من المخطط (schema validation) الحقن بالكامل؟** لا، ولا ينبغي أن يكون طبقتك الوحيدة. يزيل المخطط الصارم جزءًا كبيرًا من المدخلات المشوهة والمفرطة الحجم ويحظر الحقول غير المتوقعة، ولكن قد تكون القيمة صالحة وفقًا للمخطط ومع ذلك تكون حقن SQL أو قالب. حافظ على الاستعلامات المُعلَّمة (parameterized queries)، وإلغاء التسلسل الآمن (safe deserialization)، وترميز المخرجات (output encoding)، واستخدم المخطط لتقليص السطح الذي يجب على تلك الطبقات الدفاع عنه.
