أخطاء أمن الويب التي يقع فيها الجميع: من IDOR إلى الإسناد الجماعيّ في Laravel
تغيير رقم في رابط يكشف بيانات عميل آخر: هذه ثغرة IDOR، أشهر أسباب تسرّب البيانات. دليل دفاعيّ عمليّ لأخطر ثغرات الويب في Laravel، بأمثلة «ثغرة/إصلاح» واقعية.
مستخدمٌ عاديّ يفتح فاتورته على الرابط /invoices/1043. بدافع الفضول، يغيّر الرقم إلى 1042، فتظهر له فاتورة عميل آخر كاملةً: اسمه، مبلغه، تفاصيله. لم يخترق شيئًا، ولم يستخدم أداة، بل غيّر رقمًا في شريط العنوان. هذه ثغرة IDOR، وهي اليوم أكثر أسباب تسرّب البيانات في تطبيقات الويب شيوعًا — أكثر من حقن SQL وXSS مجتمعَين في سياق تطبيقات SaaS. والمقلق أنها لا تنتج عن كود «معقّد»، بل عن غياب سطر تحقّق واحد. لنستعرض أخطر ثغرات الويب التي يقع فيها الجميع، بأمثلة Laravel واقعية، ولكلٍّ إصلاحها.
الأولى: IDOR — حين تتحقّق من الهوية وتنسى الملكية
جوهر IDOR (اختصارًا لـ «المرجع المباشر غير الآمن للكائن») خطأٌ منطقيّ بسيط: يتحقّق تطبيقك من أن المستخدم «مسجّل دخول»، لكنه لا يتحقّق من أنه «يملك» المورد الذي يطلبه. في Laravel، أشهر صورها هي «ربط النموذج بالمسار» (Route Model Binding) دون تفويض:
// ثغرة: يعيد الفاتورة لأيّ مستخدم مسجّل، حتى لو لم يملكها
Route::get('/invoices/{invoice}', function (Invoice $invoice) {
return view('invoices.show', compact('invoice'));
})->middleware('auth');الوسيط auth يضمن أن المستخدم مسجّل، لكنه لا يفحص الملكية إطلاقًا. الإصلاح أن تربط الاستعلام بالمستخدم الحاليّ، أو تستخدم سياسة (Policy):
// إصلاح 1: قيّد الاستعلام بالمالك من البداية
Route::get('/invoices/{invoice}', function (Invoice $invoice) {
abort_if($invoice->user_id !== auth()->id(), 403);
return view('invoices.show', compact('invoice'));
})->middleware('auth');
// إصلاح 2 (الأنظف): سياسة تفصل منطق التفويض
// داخل InvoicePolicy
public function view(User $user, Invoice $invoice): bool
{
return $user->id === $invoice->user_id;
}
// في المتحكّم
$this->authorize('view', $invoice);ملاحظة مهمّة: استخدام UUID بدل الأرقام المتسلسلة يصعّب التخمين، لكنه لا يمنع IDOR. فإن تسرّب المعرّف (عبر بريد أو سجلّ أو استجابة API أخرى)، يبقى المورد مكشوفًا ما لم تتحقّق من الملكية. المعرّف الصعب ليس بديلًا عن التفويض.
الثانية: الإسناد الجماعيّ (Mass Assignment) — حقن الحقول المخفية
تخيّل نقطة نهاية لتحديث الملفّ الشخصيّ تأخذ كل المدخلات دفعةً واحدة:
// ثغرة: يقبل أيّ حقل يرسله المستخدم
public function update(Request $request)
{
auth()->user()->update($request->all());
return back();
}المشكلة أن جدول users قد يحوي عمودًا مثل is_admin أو role أو balance. لا شيء يمنع مهاجمًا من إرسال حقل إضافيّ في الطلب لم تقصد السماح به:
// هجوم: المستخدم يرسل حقلًا إضافيًّا في جسم الطلب
// POST /profile
// { "name": "أحمد", "is_admin": true }
// النتيجة: يصبح المستخدم مسؤولًا! (تصعيد صلاحيات)يوفّر Laravel حماية افتراضية عبر خاصّيتَي fillable وguarded في النموذج، لكنّ الحلّ الأمتن ألّا تثق بالمدخلات أصلًا: تحقّق منها صراحةً واسمح فقط بالحقول المقصودة:
// إصلاح: تحقّق صريح، لا يمرّ إلا المسموح
public function update(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
auth()->user()->update($validated);
return back();
}
// وفي النموذج: صرّح بالحقول القابلة للتعبئة فقط
class User extends Model
{
protected $fillable = ['name', 'email'];
}القاعدة: الحقول الحسّاسة (is_admin, role, balance, email_verified) يجب ألّا تكون قابلة للتعبئة الجماعية أبدًا. استخدم fillable (قائمة سماح) لا guarded (قائمة منع)، فالسماح الصريح أأمن من المنع، إذ ينسى أحدهم دائمًا إضافة حقل جديد للمنع.
الثالثة: حقن SQL عبر بابٍ خلفيّ — عمود الترتيب
«لكنّ Eloquent يحميني من حقن SQL!» صحيح في الاستعلامات المعتادة، لكنّ هناك بابًا يغفله كثيرون: أسماء الأعمدة. فربط القيم (Value Binding) لا يشمل أسماء الأعمدة، ما يجعل ترتيبًا يعتمد على مدخل المستخدم خطِرًا:
// ثغرة: عمود الترتيب من المستخدم مباشرةً
$users = User::query()
->orderBy($request->input('sortBy'))
->get();
// مهاجم قد يمرّر تعبيرًا خبيثًا بدل اسم عمودالإصلاح أن تتحقّق أن العمود ضمن قائمة سماح صريحة:
// إصلاح: قيّد القيمة لقائمة أعمدة مسموحة
$request->validate([
'sortBy' => 'in:name,created_at,email',
]);
$users = User::query()
->orderBy($request->validated()['sortBy'])
->get();القاعدة الأعمّ: كلّما دخل مدخل المستخدم في «بنية» الاستعلام (اسم عمود، اتّجاه ترتيب) لا في «قيمته»، فأنت مسؤول عن التحقّق منه بقائمة سماح، لأن ربط القيم لن ينقذك.
الرابعة: أخطاء الإعداد — أخطر ما يُنسى في الإنتاج
وفق تحليلات أمنية حديثة، «سوء الإعداد» هو الأكثر شيوعًا في تطبيقات Laravel الواقعية، وأخطرها على الإطلاق تفعيل وضع التنقيح في الإنتاج. فإبقاء APP_DEBUG=true يعرض تفاصيل حسّاسة عند الأخطاء، وارتبط تاريخيًّا بثغرات تنفيذ كود عن بُعد خطيرة (كـ CVE في صفحة أخطاء Ignition). القاعدة الحاسمة قبل أيّ نشر:
# في ملفّ .env بالإنتاج
APP_DEBUG=false
APP_ENV=productionويندرج تحت سوء الإعداد أيضًا: تسرّب ملفّ .env، وكشف أدوات التطوير (Telescope وHorizon) للعموم، وغياب حدّ المعدّل (Rate Limiting) على نقاط الدخول الحسّاسة كتسجيل الدخول. طبّق حدّ المحاولات لمنع هجمات التخمين:
// حدّ المعدّل: 5 محاولات في الدقيقة على تسجيل الدخول
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1');خيط يجمعها كلّها: لا تثق بالمدخلات، تحقّق من الصلاحية
لو تأمّلت الثغرات الأربع، لوجدتها تشترك في جذرٍ واحد: الثقة الضمنية بما يرسله المستخدم. IDOR يثق بأن المُعرّف يخصّ صاحبه، والإسناد الجماعيّ يثق بأن الحقول بريئة، وحقن الترتيب يثق بأن العمود صالح، وسوء الإعداد يثق بأن البيئة آمنة افتراضيًّا. الدفاع كلّه ينبع من عكس هذه الثقة: تحقّق صراحةً من كل مدخل، وافحص الملكية والصلاحية عند كل وصول لمورد.
القاعدة العملية التي تختصر كل ما سبق: الأمان ليس ميزةً تُضاف في النهاية، بل انضباطٌ يرافق كل سطر. Laravel «آمن افتراضيًّا»، لكنّ افتراضاته لا تحميك إلا حين تُستخدَم بوعي؛ فأغلب الاختراقات ليست ثغرات في الإطار، بل في طريقة استخدامنا له. اجعل التحقّق من المدخلات والتفويض عادةً لا استثناءً، وأضف فحص composer audit وأدوات التحليل الساكن (PHPStan) إلى سير عملك، وراجع نماذجك بحثًا عن حقول حسّاسة مكشوفة. من يبني بهذا الوعي، لا يستيقظ على رسالة «تسرّبت بيانات عملائنا» — لأنه أغلق الأبواب قبل أن يطرقها أحد.
هل وجدت هذا المقال مفيدًا؟
النشرة البريدية
أعجبك المحتوى؟
اشترك ليصلك كل جديد من المقالات والأخبار في بريدك مباشرةً.