تطوير واجهات REST API

أفضل ممارسات أمان API

18 دقيقة الدرس 22 من 35

أفضل ممارسات أمان API

الأمان له أهمية قصوى عند بناء واجهات API. يمكن لثغرة واحدة أن تكشف بيانات حساسة، أو تخترق حسابات المستخدمين، أو تعطل نظامك بالكامل. في هذا الدرس، سنستكشف أهم 10 مخاطر أمنية لـ OWASP API، وتعقيم المدخلات، وتكوين CORS، وأمان المحتوى، ومنع ثغرات التعيين الجماعي.

أهم 10 مخاطر أمنية لـ OWASP API

يحافظ مشروع Open Web Application Security Project (OWASP) على قائمة بأهم مخاطر أمان API. فهم هذه المخاطر هو الخطوة الأولى لبناء واجهات API آمنة.

أهم 10 مخاطر أمنية لـ OWASP API (2023):
  1. تفويض المستوى الكائني المكسور (BOLA) - يمكن للمستخدمين الوصول إلى موارد لا ينبغي لهم الوصول إليها
  2. المصادقة المكسورة - آليات مصادقة ضعيفة
  3. تفويض مستوى خاصية الكائن المكسور - التعيين الجماعي والكشف المفرط عن البيانات
  4. استهلاك الموارد غير المقيد - نقص في تحديد المعدل
  5. تفويض المستوى الوظيفي المكسور - فحوصات التفويض المفقودة على الوظائف
  6. الوصول غير المقيد لتدفقات الأعمال الحساسة - عدم وجود حماية ضد التهديدات الآلية
  7. تزوير طلب جانب الخادم (SSRF) - يجلب API موارد بعيدة دون تحقق
  8. تكوين أمان خاطئ - إعدادات أمان غير صحيحة
  9. إدارة جرد غير صحيحة - نقص في إصدارات API والتوثيق
  10. استهلاك غير آمن لواجهات API - الثقة العمياء في البيانات من واجهات API لطرف ثالث

منع تفويض المستوى الكائني المكسور (BOLA)

BOLA هو خطر الأمان رقم 1 في API. يحدث عندما لا يتحقق API بشكل صحيح من أن المستخدم لديه إذن للوصول إلى مورد معين.

// ❌ كود ضعيف Route::get('/api/v1/orders/{id}', function ($id) { $order = Order::findOrFail($id); return response()->json($order); }); // المشكلة: يمكن لأي مستخدم مصادق عليه الوصول إلى أي طلب بواسطة المعرف // ✅ كود آمن Route::get('/api/v1/orders/{id}', function ($id) { $order = Order::findOrFail($id); // التحقق من أن المستخدم المصادق عليه يملك هذا الطلب if ($order->user_id !== auth()->id()) { abort(403, 'غير مصرح بالوصول إلى هذا الطلب'); } return response()->json($order); }); // ✅ أفضل: استخدام Laravel Policy Route::get('/api/v1/orders/{order}', function (Order $order) { $this->authorize('view', $order); return new OrderResource($order); }); // app/Policies/OrderPolicy.php class OrderPolicy { public function view(User $user, Order $order): bool { return $user->id === $order->user_id || $user->hasRole('admin'); } public function update(User $user, Order $order): bool { // يمكن للمستخدمين تحديث طلباتهم المعلقة فقط return $user->id === $order->user_id && $order->status === 'pending'; } }
لا تثق أبداً في معاملات URL: تحقق دائماً من أن المستخدم المصادق عليه لديه إذن للوصول إلى المورد المحدد في URL. لا تفترض أنه لأن المستخدم يعرف المعرف، يجب أن يكون قادراً على الوصول إليه.

تعقيم المدخلات والتحقق منها

لا تثق أبداً في إدخال المستخدم. يجب التحقق من كل جزء من البيانات من العملاء وتعقيمها قبل المعالجة.

<?php // app/Http/Requests/CreateUserRequest.php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Password; class CreateUserRequest extends FormRequest { public function authorize(): bool { return true; } public function rules(): array { return [ 'name' => [ 'required', 'string', 'max:255', 'regex:/^[a-zA-Z\s]+$/' // حروف ومسافات فقط ], 'email' => [ 'required', 'email:rfc,dns', // يتحقق من امتثال RFC و DNS 'max:255', 'unique:users,email' ], 'password' => [ 'required', 'confirmed', Password::min(8) ->mixedCase() ->numbers() ->symbols() ->uncompromised() // يتحقق من قاعدة بيانات كلمات المرور المسربة ], 'phone' => [ 'nullable', 'regex:/^\+?[1-9]\d{1,14}$/' // صيغة E.164 ], 'website' => [ 'nullable', 'url', 'active_url' // يتحقق من وجود سجل DNS ], 'bio' => [ 'nullable', 'string', 'max:1000' ] ]; } public function messages(): array { return [ 'email.email' => 'يرجى تقديم عنوان بريد إلكتروني صالح', 'password.uncompromised' => 'تم كشف كلمة المرور هذه في خرق للبيانات. يرجى اختيار كلمة مرور أخرى.', 'name.regex' => 'يمكن أن يحتوي الاسم على حروف ومسافات فقط' ]; } // تعقيم المدخلات بعد التحقق protected function passedValidation(): void { $this->merge([ 'name' => strip_tags($this->name), 'email' => strtolower(trim($this->email)), 'bio' => $this->bio ? strip_tags($this->bio) : null ]); } } // الاستخدام في المتحكم public function store(CreateUserRequest $request) { // $request->validated() يعيد البيانات المتحقق منها والمعقمة فقط $user = User::create($request->validated()); return new UserResource($user); }

منع حقن SQL

استخدم دائماً استعلامات محددة المعاملات أو طرق ORM. لا تدمج أبداً إدخال المستخدم في استعلامات SQL.

<?php // ❌ ضعيف أمام حقن SQL $email = $request->input('email'); $user = DB::select("SELECT * FROM users WHERE email = '$email'"); // إذا أرسل المستخدم: admin@example.com' OR '1'='1 // يصبح الاستعلام: SELECT * FROM users WHERE email = 'admin@example.com' OR '1'='1' // هذا يعيد جميع المستخدمين! // ✅ آمن: استخدام ربط المعاملات $email = $request->input('email'); $user = DB::select('SELECT * FROM users WHERE email = ?', [$email]); // ✅ أفضل: استخدام Query Builder $user = DB::table('users')->where('email', $email)->first(); // ✅ الأفضل: استخدام Eloquent ORM $user = User::where('email', $email)->first(); // أفضل مع النطاقات class User extends Model { public function scopeByEmail($query, string $email) { return $query->where('email', $email); } } $user = User::byEmail($email)->first();

حماية التعيين الجماعي

تحدث ثغرات التعيين الجماعي عندما يمكن للمستخدمين تعديل حقول لا ينبغي لهم الوصول إليها.

<?php // ❌ كود ضعيف class User extends Model { // لا حماية! } Route::post('/api/v1/users', function (Request $request) { $user = User::create($request->all()); return response()->json($user); }); // المهاجم يرسل: // { // "name": "John", // "email": "john@example.com", // "is_admin": true, ← لا ينبغي أن يكون قابلاً للتعيين بواسطة المستخدم! // "balance": 1000000 ← لا ينبغي أن يكون قابلاً للتعيين بواسطة المستخدم! // } // ✅ آمن: استخدم $fillable أو $guarded class User extends Model { // الخيار 1: قائمة بيضاء بالحقول المسموحة protected $fillable = [ 'name', 'email', 'password', 'bio' ]; // الخيار 2: قائمة سوداء بالحقول الممنوعة protected $guarded = [ 'id', 'is_admin', 'balance', 'created_at', 'updated_at' ]; // اجعل الحقول الحساسة غير مرئية في JSON protected $hidden = [ 'password', 'remember_token', 'two_factor_secret' ]; } // أفضل ممارسة: مرر البيانات المتحقق منها فقط Route::post('/api/v1/users', function (CreateUserRequest $request) { // $request->validated() يحتوي على الحقول المسموح بها فقط $user = User::create($request->validated()); return new UserResource($user); });
استخدم دائماً Form Requests: توفر فئات Form Request التحقق والترخيص وتضمن تمرير البيانات المتحقق منها فقط إلى نماذجك. هذا يمنع ثغرات التعيين الجماعي تلقائياً.

تكوين CORS

يتحكم مشاركة الموارد عبر الأصول (CORS) في النطاقات التي يمكنها الوصول إلى API الخاص بك من المتصفحات. يمكن أن يعرض CORS المكون بشكل خاطئ API الخاص بك للهجمات.

<?php // config/cors.php return [ // ❌ متساهل جداً 'paths' => ['api/*'], 'allowed_methods' => ['*'], 'allowed_origins' => ['*'], // يسمح بأي نطاق! 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => true, // خطير مع allowed_origins: * // ✅ تكوين آمن 'paths' => ['api/*'], 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 'allowed_origins' => [ 'https://myapp.com', 'https://www.myapp.com', 'https://admin.myapp.com' ], 'allowed_origins_patterns' => [ '/^https:\/\/([a-z]+\.)?myapp\.com$/' ], 'allowed_headers' => [ 'Content-Type', 'Authorization', 'X-Requested-With', 'Accept' ], 'exposed_headers' => [ 'X-Total-Count', 'X-Page-Count' ], 'max_age' => 86400, // 24 ساعة 'supports_credentials' => true, ]; // للتكوين المستند إلى البيئة return [ 'allowed_origins' => array_filter([ env('FRONTEND_URL'), env('ADMIN_URL'), ]), ];
لا تستخدم أبداً `allowed_origins: ['*']` مع `supports_credentials: true`! هذا المزيج يسمح لأي موقع ويب بإجراء طلبات مصادق عليها إلى API الخاص بك نيابة عن مستخدميك، مما يؤدي إلى هجمات CSRF.

سياسة أمان المحتوى (CSP)

بينما تستخدم بشكل أساسي لصفحات الويب، يجب على واجهات API أيضاً تنفيذ رؤوس الأمان لمنع الهجمات المختلفة.

<?php // app/Http/Middleware/SecurityHeaders.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class SecurityHeaders { public function handle(Request $request, Closure $next) { $response = $next($request); // منع استنشاق نوع MIME $response->headers->set('X-Content-Type-Options', 'nosniff'); // منع اختطاف النقرات $response->headers->set('X-Frame-Options', 'DENY'); // تمكين حماية XSS $response->headers->set('X-XSS-Protection', '1; mode=block'); // أمان النقل الصارم (فرض HTTPS) $response->headers->set( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload' ); // سياسة أمان المحتوى $response->headers->set( 'Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'" ); // سياسة المُحيل $response->headers->set('Referrer-Policy', 'no-referrer'); // سياسة الأذونات (كانت سابقاً سياسة الميزات) $response->headers->set( 'Permissions-Policy', 'geolocation=(), microphone=(), camera=()' ); return $response; } } // التسجيل في app/Http/Kernel.php protected $middlewareGroups = [ 'api' => [ SecurityHeaders::class, // ... وسيطات أخرى ], ];

تحديد المعدل والتقييد

احمِ API الخاص بك من الإساءة وهجمات حجب الخدمة باستخدام تحديد المعدل.

<?php // app/Http/Kernel.php protected $middlewareGroups = [ 'api' => [ \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', ], ]; // config/routes.php - تحديد حدود المعدل use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for('api', function (Request $request) { return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); }); // حدود مختلفة لنقاط نهاية مختلفة RateLimiter::for('login', function (Request $request) { return Limit::perMinute(5)->by($request->ip()) ->response(function () { return response()->json([ 'errors' => [[ 'status' => '429', 'title' => 'طلبات كثيرة جداً', 'detail' => 'يرجى الانتظار قبل محاولة تسجيل الدخول مرة أخرى' ]] ], 429); }); }); RateLimiter::for('uploads', function (Request $request) { return $request->user()->isPremium() ? Limit::perMinute(100) : Limit::perMinute(10); }); // التطبيق في المسارات Route::middleware(['throttle:login'])->group(function () { Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']); }); Route::middleware(['auth:sanctum', 'throttle:uploads'])->group(function () { Route::post('/upload', [UploadController::class, 'store']); });

تخزين كلمة المرور الآمنة

<?php // ❌ لا تخزن أبداً كلمات المرور بنص عادي $user->password = $request->password; // ❌ لا تستخدم أبداً التجزئة الضعيفة $user->password = md5($request->password); $user->password = sha1($request->password); // ✅ استخدم bcrypt (افتراضي في Laravel) $user->password = Hash::make($request->password); // ✅ أفضل: استخدم Argon2id (أكثر أماناً، قابل للتكوين) // config/hashing.php return [ 'driver' => 'argon2id', 'argon' => [ 'memory' => 65536, 'threads' => 4, 'time' => 4, ], ]; $user->password = Hash::make($request->password); // التحقق من كلمات المرور if (Hash::check($request->password, $user->password)) { // كلمة المرور متطابقة } // التحقق من حاجة كلمة المرور لإعادة التجزئة (الخوارزمية تغيرت) if (Hash::needsRehash($user->password)) { $user->password = Hash::make($request->password); $user->save(); }

منع هجمات التوقيت

<?php // ❌ ضعيف أمام هجمات التوقيت public function login(Request $request) { $user = User::where('email', $request->email)->first(); if (!$user) { return response()->json(['error' => 'بريد إلكتروني غير صالح'], 401); } if (!Hash::check($request->password, $user->password)) { return response()->json(['error' => 'كلمة مرور غير صالحة'], 401); } // تسجيل الدخول ناجح } // المشكلة: يختلف وقت الاستجابة بناءً على وجود البريد الإلكتروني // ✅ آمن: قم دائماً بتنفيذ نفس العمليات public function login(Request $request) { $user = User::where('email', $request->email)->first(); // قم دائماً بتجزئة كلمة المرور، حتى لو لم يكن المستخدم موجوداً $passwordValid = $user && Hash::check( $request->password, $user->password ); if (!$passwordValid) { // نفس الاستجابة بغض النظر عن وجود البريد الإلكتروني return response()->json([ 'errors' => [[ 'status' => '401', 'title' => 'فشلت المصادقة', 'detail' => 'بيانات اعتماد غير صالحة' ]] ], 401); } // تسجيل الدخول ناجح }
تمرين:
  1. قم بمراجعة نقطة نهاية API موجودة بحثاً عن ثغرات BOLA
  2. قم بتنفيذ Form Request شامل مع التحقق والتعقيم
  3. قم بتكوين CORS بشكل صحيح لبيئة إنتاج
  4. أضف وسيطة رؤوس الأمان إلى API الخاص بك
  5. قم بتنفيذ تحديد معدل متدرج (حدود مختلفة للمستخدمين المجانيين مقابل المميزين)
  6. راجع نماذجك وتأكد من أن لديها جميعاً خصائص $fillable أو $guarded مناسبة
الأمان عملية مستمرة: الأمان ليس مهمة لمرة واحدة. راجع كودك بانتظام، وابقَ محدثاً على الثغرات الجديدة، واستخدم أدوات فحص التبعيات، وأجرِ اختبارات اختراق على واجهات API الخاصة بك.