سطر واحد من الكود يمكنه تقويض نموذج التحكم في الوصول الخاص بك بالكامل. إذا قمت بتمرير جسم الطلب (request body) مباشرة إلى عملية تحديث في قاعدة البيانات، فقد منحت العميل قلماً لإعادة كتابة المخطط (schema) الخاص بك. هذا هو "التعيين الجماعي" (Mass Assignment). إنه ليس خطأً غريباً أو حالة نادرة، بل هو فشل في التصميم يظهر كلما تعاملت واجهة برمجة التطبيقات (API) مع الحمولة (payload) كأنها سياسة التحديث الخاصة بها.

await db.users.update(req.params.id, { ...req.body });

يبدو الأمر نظيفاً، ويوفر عليك عناء الكتابة. لكن العميل هو من يتحكم في المفاتيح (keys). يمكن للمهاجم إضافة "role": "admin" أو "accountId": "someone_else" أو "credit": 99999" إلى عملية تحديث ملف شخصي عادية تماماً. قد تتحقق طبقة التحقق (validation layer) لديك مما إذا كانت هذه القيم عبارة عن نصوص أو أرقام وتقول إنها تبدو جيدة. ومع ذلك، فإن "الصلاحية" (Validity) لا تعني "التفويض" (Authorization). قد يمتلك المستخدم السجل المستهدف بشكل مشروع، لكن هذا لا يعني أن له الحق في تعديل كل حقل بداخله.

كيف يبدو التعيين الجماعي في الواقع

يكمن الخطر في سهولة الاستخدام. تجعل أطر العمل (Frameworks) وتقنيات الـ ORMs من السهل جداً ربط مفاتيح JSON مباشرة بأعمدة قاعدة البيانات. عندما تفعل ذلك، فأنت تخبر قاعدة البيانات بأن تثق في العميل بشأن ما الذي يجب تغييره، وليس فقط كيفية تغييره.

قد يقوم مستخدم يقوم بتحديث ملفه الشخصي بإرسال بيانات صالحة لـ displayName و bio ، ولكنه يمرر معها role أو balance. إذا كان المتحكم (controller) الخاص بك يقوم ببساطة بتمرير الكائن (object)، فستقوم قاعدة البيانات بكتابة كل شيء. تقوم عملية التحقق (Validation) باكتشاف القيم المشوهة، لكنها نادراً ما تكتشف المفاتيح الخبيثة. وبذلك تصبح قاعدة العمل التي تقول "يُسمح لهذا المستخدم بتحديث ملفه الشخصي" بمثابة إذن شامل لكل عمود في الصف.

الحل ليس في زيادة عمليات التحقق، بل في بناء بنية أكثر صرامة.

البوابات الثلاث

تمر أي عملية تعديل (mutation) آمنة عبر ثلاث عمليات فحص منفصلة قبل أن تلمس وحدة التخزين.

الحقول المقبولة (القائمة المسموح بها - Allowlist)

ابدأ بتحديد المفاتيح التي ستنظر إليها بالضبط. إذا لم يكن الحقل موجوداً في القائمة المسموح بها، فارفض الطلب أو احذف المفتاح. هذا يقلب الوضع الافتراضي: تصبح أعمدة قاعدة البيانات الجديدة غير قابلة للكتابة حتى يقوم المطور بالكشف عنها صراحةً. تنمو المخططات (Schemas) بمرور الوقت؛ فقد يضيف زميل لك stripeCustomerId أو departmentBudget أو علامة isVerified. باستخدام القائمة المسموح بها، يتم حماية هذه الأعمدة الجديدة تلقائياً من عمليات الكتابة من قبل العميل. وبدونها، يصبح كل عمود جديد بمثابة سطح API معرض للاختراق عن غير قصد.

القيم الصالحة

بمجرد معرفة الحقول المسموح بها، تحقق مما إذا كانت القيم منطقية. هل سلسلة المنطقة الزمنية (timezone string) هي منطقة زمنية معترف بها حقاً؟ هل تنسيق البريد الإلكتروني صحيح؟ هل الرقم ضمن نطاق معقول؟ هذا يندرج تحت بند "النظافة البرمجية" (hygiene)؛ فهو يمنع دخول البيانات العشوائية إلى نظامك، لكنه لا يمنع الإساءة. فالسلسلة النصية "admin" الصالحة تماماً تظل خطيرة في حقل role إذا أرسلها الشخص الخطأ.

الانتقالات المصرح بها

هذه هي البوابة التي تتخطاها معظم الفرق، وهي المكان الذي تكمن فيه الحماية الحقيقية. اطرح سؤالاً دقيقاً: هل يمتلك هذا الفاعل (actor) تحديداً الإذن لتعديل هذا الحقل المحدد في هذا السجل المحدد؟ ليس "هل المستخدم مسؤول (admin)؟" ولا "هل يمتلك المستخدم نطاق write:users؟" بل "هل يُسمح لهذا المستخدم بتغيير displayName الخاص به، ولكن ليس accountId أبداً؟". إن التفويض لكل حقل (Per-field authorization) يمنع الأذونات الواسعة مثل "محرر" (Editor) أو "مستخدم" (User) من أن تصبح مفتاحاً رئيسياً لكل خاصية في الصف.

بناء دالة التعديل (Patch Function)

اربط البوابات الثلاث في مسار (pipeline) واحد. عندما يصل طلب تعديل (patch request)، قم بتمريره عبر المراحل بالترتيب.

أولاً، قم بتصفية المدخلات مقابل القائمة المسموح بها. إذا لم يكن role حقلاً مسموحاً به لهذا المسار (endpoint)، فتوقف عند هذا الحد. لا يوجد سبب للتحقق من قيمة أو تفويضها بينما لا ينبغي أن تكون قد استلمتها أصلاً.

ثانياً، تحقق من صحة القيم المسموح بها. افحص الأنواع، والتنسيقات، وقواعد العمل. يجب أن يكون حقل الموقع عبارة عن نص يمثل منطقة زمنية حقيقية. يجب أن يكون رابط الصورة الشخصية (avatar URL) عبارة عن URI صالح وبطول معين.

ثالثاً، صرح بالإجراء. تحقق من أن الفاعل يمتلك السجل المستهدف، أو يمتلك الإذن الدقيق المطلوب لهذا الحقل. تعتبر الملكية خياراً افتراضياً جيداً للبيانات الشخصية، ولكن بعض الحقول لا تزال بحاجة إلى بوابات إضافية. قد يمتلك المستخدم ملفه الشخصي، ومع ذلك لا ينبغي إلا لمسؤول الفواتير (billing admin) أن يلمس حقل taxRegion.

رابعاً، قم بتوحيد البيانات (normalize). قم بإزالة المسافات الزائدة، ودمج المسافات المتكررة، وتحويل رسائل البريد الإلكتروني إلى أحرف صغيرة، أو إزالة أحرف التحكم. افعل ذلك بعد عملية التحقق ولكن قبل التخزين حتى لا تضطر إلى مقارنة نصوص غير نظيفة أثناء عمليات فحص التفويض.

If the input fails any gate, reject the entire mutation. Do not partially apply the safe fields and quietly drop the bad ones. A mixed response trains clients to spray every key they can think of and see what sticks. Fail explicitly.

The Edge Cases That Actually Matter

Mass assignment defenses live or die on details that unit tests often miss.

Duplicate JSON keys. Attackers can send payloads like {"role": "user", "role": "admin"}. Depending on your HTTP parser and framework, the second value might overwrite the first before your application code sees the object. Test this behavior at the parser level. If your framework silently accepts the last key, your allowlist might be looking at "user" while the database receives "admin".

Nested objects, nulls, and arrays. Do not assume the payload is flat. A client might wrap a restricted field inside a nested object like { "profile": { "role": "admin" } }. Your allowlist must recurse if your schema does. Likewise, decide how you handle null. Does it mean "ignore this field" or "delete this field"? And if an array is expected, does your validator reject unexpected structures, or does it cast a single object into an array and let it through?

Unicode normalization. Two strings can look identical to a human while being different sequences of bytes. A user might send a precomposed é or a decomposed e plus combining accent. If your authorization check normalizes once but your storage layer normalizes differently, you can end up with inconsistent data or, worse, a bypass where a username collision slips past your logic. Normalize early and normalize consistently.

Race conditions. Authorization decisions are not freeze frames. They happen at a point in time. Two requests can read the same record, both see that the actor is allowed to write, and both issue updates. In between, the state or the actor’s permissions might have changed. Always apply database updates with a condition on a version number or a state machine value. Use something like UPDATE users SET ... WHERE id = ? AND version = 5. If the row changed since you read it, the write fails. Handle the failure by retrying or rejecting. This keeps stale authorization checks from corrupting your data.

Monitor What Matters

You cannot secure what you cannot see. Build your audit logging around the decision, not just the action.

Log the actor ID and the target ID. Log the exact field names that were accepted and the ones that were rejected. Log the policy version that made the decision and the final result. If a user suddenly starts getting role rejected in their profile update, you want to know immediately.

Never log bearer tokens. Never dump entire request bodies into your logs. An audit trail should help you investigate abuse, not become a repository of credentials and personal data.

The Only Rule You Need

A request body proposes data. It never defines its own authority. The client can ask for anything. Your server decides, field by field and row by row, what is allowed to land in permanent storage. Build your patches with that separation in mind, and mass assignment becomes a problem you stopped long before it reached your authorization layer.