API غير الآمن هو باب مفتوح للهاكرز. في هذا الدليل العملي، سنفكك كل طبقة أمنية من Authentication إلى Rate Limiting، مع أكواد حقيقية وحيل من تجارب شركات مثل GitHub وStripe لتجنب الثغرات قبل أن تستغل.
في عام ٢٠٢٣، تعرضت شركة Okta لخرق بيانات بسبب ثغرة في API لم يتم تأمينها بشكل صحيح، مما أدى إلى تسريب معلومات حساسة لـ ٣٦٦ شركة عملاقة. القصة لم تكن عن ضعف في التشفير، بل عن نسيان بسيط: عدم تفعيل Rate Limiting على endpoint واحد فقط. عندما اكتشف المهاجمون هذا الـ endpoint، أرسلوا ١٠٠ ألف طلب في دقيقة واحدة، ما أدى إلى كشف بيانات المستخدمين بسبب الـ brute force. الحقيقة المحرجة هي أن ٩٠٪ من الـ APIs في الشركات الناشئة اليوم لديها ثغرات مشابهة، لكن المطورين إما لا يعرفون كيف يؤمنونها، أو يعتقدون أن الـ security هو مشكلة الـ DevOps فقط. اليوم سنثبت العكس: تأمين API هو مسؤوليتك كمبرمج، ويمكنك فعله بكفاءة دون أن تضيع أسبوعاً كاملاً في البحث عن أفضل الممارسات.
عندما نتحدث عن تأمين API، لا نتحدث عن طبقة واحدة، بل عن سلسلة من الطبقات المترابطة مثل طبقات البصل. إذا اخترقت طبقة واحدة، تبقى الطبقات الأخرى صامدة. المشكلة أن معظم المطورين يركزون فقط على الـ Authentication ويهملون بقية الطبقات، وهذا خطأ قاتل. في هذا المقال، سنغطي كل طبقة بالتفصيل، بدءاً من الـ Authentication وصولاً إلى الـ Rate Limiting، مع شرح ماذا يحدث بالضبط في الذاكرة والمعالج عندما يتم تنفيذ كل خطوة، وكيف يمكن للمهاجمين استغلال الثغرات إذا لم يتم تنفيذها بشكل صحيح.
أول خطوة في تأمين API هي التأكد من أن الشخص الذي يرسل الطلب هو فعلاً من يدعي أنه هو. الـ Authentication هو البوابة الأولى، وإذا كانت ضعيفة، فكل الطبقات الأخرى بلا فائدة. المشكلة أن الكثير من المطورين يعتمدون على طرق قديمة مثل الـ Basic Auth أو حتى الـ API Keys الثابتة، وهي طرق سهلة الاختراق. الـ Basic Auth مثلاً، يرسل الـ credentials مشفرة بـ Base64 فقط، وهي ليست تشفيراً حقيقياً، بل مجرد ترميز يمكن فكه بسهولة. إذا كان الـ API الخاص بك يستخدم الـ Basic Auth، فاعلم أن أي شخص يستطيع اعتراض الـ request يمكنه الحصول على الـ username و password خلال ثوانٍ.
الحل الأفضل هو استخدام الـ JWT (JSON Web Tokens) مع الـ OAuth 2.0. الـ JWT هو معيار مفتوح يسمح بنقل البيانات بين الأطراف بشكل آمن، وهو يحتوي على ثلاث أجزاء: الـ Header، الـ Payload، والـ Signature. الـ Signature هو الجزء الأهم، لأنه يضمن أن الـ token لم يتم التلاعب به. عندما يرسل العميل طلباً إلى الـ API، يرسل معه الـ JWT في الـ Authorization header. السيرفر يستقبل الـ token، يفك الـ Signature باستخدام الـ secret key، وإذا تطابق مع الـ payload، يسمح بالوصول. لكن هنا تكمن المشكلة: إذا تم تسريب الـ secret key، يمكن لأي شخص توليد tokens مزيفة. لهذا السبب يجب تخزين الـ secret key في بيئة آمنة مثل الـ AWS Secrets Manager أو الـ HashiCorp Vault، وليس في ملفات الـ config أو الـ environment variables العادية.
// مثال على توليد JWT باستخدام Node.js
const jwt = require('jsonwebtoken');
const secretKey = process.env.JWT_SECRET; // يجب أن يكون هذا في بيئة آمنة
function generateToken(user) {
const payload = {
id: user.id,
email: user.email,
role: user.role // مهم لتطبيق الـ Role-Based Access Control
};
// الـ token ينتهي صلاحيته بعد ساعة
return jwt.sign(payload, secretKey, { expiresIn: '1h' });
}
// التحقق من الـ token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, secretKey);
return decoded;
} catch (err) {
// إذا كان الـ token غير صالح أو انتهت صلاحيته
throw new Error('Invalid or expired token');
}
}
// استخدام الـ middleware في Express لحماية الـ endpoints
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; //Bearer TOKEN
if (!token) return res.sendStatus(401);
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (err) {
return res.sendStatus(403);
}
}الـ JWT يبدو بسيطاً، لكن هناك عدة فخاخ شائعة يقع فيها المطورون. الأول هو عدم تحديد مدة صلاحية للـ token. إذا تركت الـ token صالحاً إلى الأبد، فهذا يعني أنه إذا تم تسريبه، يمكن استخدامه مدى الحياة. الحل هو تحديد مدة صلاحية قصيرة، مثلاً ساعة واحدة، واستخدام الـ refresh tokens لإعادة إصدار الـ access tokens دون الحاجة لتسجيل الدخول مرة أخرى. الـ refresh token يجب أن يكون له مدة صلاحية أطول، مثلاً ٣٠ يوماً، ويجب تخزينه في مكان آمن مثل الـ HTTP-only cookies لمنع سرقة الـ token عبر هجمات الـ XSS.
الفخ الثاني هو عدم التحقق من الـ signature بشكل صحيح. بعض المكتبات تسمح بتجاوز التحقق من الـ signature إذا كان الـ token غير موقّع، وهذا خطأ كبير. يجب دائماً التحقق من أن الـ token موقّع باستخدام الـ secret key الصحيح. الفخ الثالث هو استخدام الـ payload لتخزين بيانات حساسة مثل كلمات المرور أو أرقام البطاقات الائتمانية. الـ JWT ليس مشفراً، بل هو مجرد ترميز يمكن لأي شخص فكه باستخدام أدوات مثل jwt.io. لذلك، يجب تخزين البيانات الحساسة في قاعدة البيانات فقط، واستخدام الـ JWT لتخزين الـ identifiers فقط.
بعد أن تتأكد من هوية المستخدم، تأتي الخطوة التالية: التأكد من أنه مسموح له بفعل ما يريد فعله. الـ Authorization هو ما يحدد الصلاحيات، وإذا تم تنفيذه بشكل خاطئ، يمكن للمستخدمين العاديين الوصول إلى بيانات أو وظائف مخصصة للمسؤولين فقط. المشكلة أن الكثير من المطورين يعتمدون على الـ role-based access control (RBAC) بشكل سطحي، دون التفكير في السيناريوهات الحقيقية. مثلاً، قد يكون لدى المستخدم دور
# مثال على تطبيق RBAC في Flask
from functools import wraps
from flask import request, jsonify
# أدوار المستخدمين
ROLES = {
'user': ['read'], # يمكن القراءة فقط
'editor': ['read', 'create', 'update'], # يمكن القراءة والكتابة
'admin': ['read', 'create', 'update', 'delete', 'manage_users'] # صلاحيات كاملة
}
def role_required(*required_roles):
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
# الحصول على دور المستخدم من الـ JWT (المفترض أنه تم فكه في الـ middleware)
user_role = getattr(request, 'user_role', None)
if not user_role:
return jsonify({'error': 'Unauthorized'}), 401
# التحقق من أن المستخدم لديه الصلاحية المطلوبة
if user_role not in ROLES:
return jsonify({'error': 'Forbidden'}), 403
# الحصول على الإجراء المطلوب (مثال: 'delete')
action = kwargs.get('action')
if action not in ROLES[user_role]:
return jsonify({'error': 'Forbidden'}), 403
return f(*args, **kwargs)
return wrapped
return decorator
# استخدام الـ decorator لحماية الـ endpoints
@app.route('/posts/<int:post_id>', methods=['DELETE'])
@role_required(action='delete')
# إذا كان المستخدم ليس admin، سيرجع 403
def delete_post(post_id):
# منطق حذف المنشور
return jsonify({'message': 'Post deleted'})لكن الـ RBAC وحده لا يكفي في بعض الحالات. مثلاً، في تطبيقات مثل GitHub، قد يكون لدى المستخدم صلاحية تحرير مستودع معين، لكن ليس لديه صلاحية حذف المستودع. هنا يأتي دور الـ Attribute-Based Access Control (ABAC)، حيث يتم تحديد الصلاحيات بناءً على سمات المستخدم والمورد. مثلاً، يمكن السماح للمستخدم بتحرير ملف معين إذا كان هو مالك الملف أو إذا كان الملف موجوداً في مستودع يملكه. الـ ABAC أكثر مرونة من الـ RBAC، لكنه أيضاً أكثر تعقيداً في التنفيذ. إذا كنت تعمل على تطبيق معقد، قد تحتاج إلى مكتبة مثل Casbin أو Oso لتسهيل إدارة الصلاحيات.
الفخ الشائع في الـ Authorization هو عدم التحقق من الصلاحيات على مستوى الـ database queries. مثلاً، قد يكون لديك endpoint لحذف منشور، وتتحقق من أن المستخدم لديه صلاحية الحذف، لكنك تنسى التحقق من أن المنشور ينتمي للمستخدم في الاستعلام نفسه. هذا يمكن أن يؤدي إلى ثغرة تسمى Insecure Direct Object Reference (IDOR)، حيث يمكن للمهاجم تغيير الـ ID في الـ request للحصول على بيانات مستخدم آخر. الحل هو دائماً التحقق من الصلاحيات على مستوى الـ database query، وليس فقط على مستوى الـ endpoint.
الـ Input Validation هو الخط الدفاعي الأول ضد الهجمات مثل الـ SQL Injection والـ XSS. المشكلة أن الكثير من المطورين يعتمدون على الـ client-side validation فقط، وهو غير كافٍ أبداً. الـ client-side validation يمكن تجاوزه بسهولة باستخدام أدوات مثل Postman أو حتى الـ browser's developer tools. يجب دائماً تنفيذ الـ validation على الـ server-side، ويجب أن يكون صارماً للغاية. مثلاً، إذا كان الـ endpoint يتوقع رقم هاتف، يجب التحقق من أن المدخل يحتوي على أرقام فقط، وأن طوله مناسب، وأن يبدأ برمز الدولة الصحيح. إذا كان المدخل لا يتطابق مع التوقعات، يجب رفض الطلب فوراً وإرسال رسالة خطأ واضحة.
لكن الـ validation وحده لا يكفي. يجب أيضاً استخدام الـ sanitization لتنظيف المدخلات من أي رموز خطيرة. مثلاً، في حالة الـ SQL queries، يجب استخدام الـ parameterized queries بدلاً من الـ string concatenation لمنع الـ SQL Injection. في حالة الـ HTML output، يجب استخدام مكتبات مثل DOMPurify لتنظيف المدخلات من أي أكواد JavaScript ضارة لمنع الـ XSS. المشكلة أن الكثير من المطورين يعتقدون أن استخدام الـ ORM يحميهم من الـ SQL Injection، وهذا ليس صحيحاً دائماً. بعض الـ ORMs تسمح بتنفيذ الـ raw queries، وإذا استخدمت الـ string concatenation معها، ستظل معرضاً للهجوم.
// مثال على Input Validation و Sanitization في Node.js
const { body, validationResult } = require('express-validator');
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const purify = DOMPurify(window);
// قواعد الـ validation لـ endpoint إنشاء منشور
const validatePost = [
body('title').trim().notEmpty().withMessage('Title is required')
.isLength({ max: 100 }).withMessage('Title must be less than 100 characters'),
body('content').trim().notEmpty().withMessage('Content is required')
.isLength({ max: 5000 }).withMessage('Content must be less than 5000 characters'),
body('tags').optional().isArray().withMessage('Tags must be an array')
.custom(tags => tags.every(tag => typeof tag === 'string' && tag.length <= 50))
.withMessage('Each tag must be a string less than 50 characters')
];
// استخدام الـ validation في الـ endpoint
app.post('/posts', validatePost, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Sanitize الـ content لمنع الـ XSS
const sanitizedC purify.sanitize(req.body.content);
// إنشاء المنشور في قاعدة البيانات
// ...
res.status(201).json({ message: 'Post created' });
});الفخ الشائع في الـ Input Validation هو الاعتماد على الـ regex بدون فهم عميق لكيفية عمله. مثلاً، قد تستخدم regex للتحقق من عنوان بريد إلكتروني، لكن إذا كان الـ regex غير دقيق، يمكن للمهاجمين تجاوز الـ validation باستخدام عناوين بريد إلكتروني غير صالحة. الحل هو استخدام مكتبات جاهزة مثل validator.js في Node.js أو Django's validators في Python، بدلاً من كتابة الـ regex بنفسك. أيضاً، يجب دائماً التحقق من الـ content type في الـ headers. إذا كان الـ endpoint يتوقع JSON، يجب التحقق من أن الـ Content-Type هو application/json، وليس text/plain أو أي نوع آخر.
الـ Rate Limiting هو ما يمنع المهاجمين من إرسال آلاف الطلبات في الثانية الواحدة، سواء كان ذلك لهجمات الـ brute force أو الـ DDoS. المشكلة أن الكثير من المطورين يعتقدون أن الـ Rate Limiting هو مسؤولية الـ DevOps أو الـ cloud provider، وهذا خطأ. يجب تنفيذ الـ Rate Limiting على مستوى الـ application لحماية الـ endpoints الحساسة مثل الـ login و الـ password reset. إذا لم تفعل ذلك، يمكن للمهاجمين استغلال هذه الـ endpoints لكشف كلمات المرور أو إرسال آلاف رسائل البريد الإلكتروني الاحتيالية.
هناك عدة طرق لتنفيذ الـ Rate Limiting، لكن الأكثر شيوعاً هو استخدام الـ token bucket أو الـ sliding window algorithms. الـ token bucket يعمل عن طريق إعطاء كل عميل عدداً محدداً من الـ tokens في فترة زمنية معينة. كل طلب يستهلك token، وعندما تنفذ الـ tokens، يتم رفض الطلبات الجديدة. الـ sliding window أكثر دقة، لكنه أيضاً أكثر تعقيداً في التنفيذ. في معظم الحالات، يمكنك استخدام مكتبات جاهزة مثل express-rate-limit في Node.js أو django-ratelimit في Python.
// مثال على Rate Limiting في Express باستخدام express-rate-limit
const rateLimit = require('express-rate-limit');
// تحديد الـ limiter لـ login endpoint
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 دقيقة
max: 5, // 5 محاولات فقط
message: 'Too many login attempts, please try again later.',
standardHeaders: true, // إرجاع الـ rate limit info في الـ headers
legacyHeaders: false, // عدم إرجاع الـ headers القديمة
keyGenerator: (req) => {
// استخدام الـ IP و الـ username لمنع الـ brute force
return req.ip + (req.body.email || '');
},
skip: (req) => {
// تجاوز الـ limiter للمستخدمين الذين لديهم token صالح
return req.user !== undefined;
}
});
// تطبيق الـ limiter على الـ login endpoint
app.post('/login', loginLimiter, (req, res) => {
// منطق الـ login
});
// تحديد الـ limiter عام لجميع الـ endpoints
const generalLimiter = rateLimit({
windowMs: 60 * 1000, // دقيقة واحدة
max: 100, // 100 طلب لكل عميل
message: 'Too many requests, please try again later.'
});
// تطبيق الـ limiter العام على جميع الـ endpoints
app.use(generalLimiter);الفخ الشائع في الـ Rate Limiting هو عدم التمييز بين الـ endpoints المختلفة. مثلاً، قد تضع حداً واحداً لجميع الـ endpoints، وهذا خطأ. الـ login endpoint يجب أن يكون لديه حد أقل بكثير من الـ endpoints العامة مثل الـ homepage. أيضاً، يجب أن يكون الـ Rate Limiting موزعاً، وليس محلياً فقط. إذا كان لديك عدة سيرفرات، يجب استخدام قاعدة بيانات موزعة مثل Redis لتخزين الـ rate limits، وإلا يمكن للمهاجمين تجاوز الـ limits عن طريق إرسال الطلبات إلى سيرفرات مختلفة. في شركات مثل Stripe، يستخدمون أنظمة معقدة مثل NGINX مع modules مخصصة للـ Rate Limiting، لكنهم أيضاً يعتمدون على مكتبات مثل RedisRateLimiter لضمان التوزيع.
هناك أيضاً مشكلة الـ false positives في الـ Rate Limiting، حيث قد يتم حظر مستخدمين حقيقيين بسبب نشاطهم الطبيعي. مثلاً، إذا كان لديك تطبيق جوال يرسل طلبات متكررة لتحديث البيانات، قد يتم حظر المستخدمين بسبب الـ Rate Limiting. الحل هو استخدام تقنيات مثل الـ exponential backoff، حيث يتم زيادة وقت الانتظار بين الطلبات بدلاً من حظر المستخدم فوراً. أيضاً، يمكن استخدام الـ CAPTCHA بعد عدد معين من المحاولات لمنع الروبوتات دون حظر المستخدمين الحقيقيين.
حتى إذا قمت بتأمين الـ API بشكل كامل، لا يزال هناك احتمال لحدوث اختراقات. لهذا السبب، يجب أن يكون لديك نظام قوي للـ Logging و الـ Monitoring. الـ Logging ليس مجرد تسجيل الأخطاء، بل هو تسجيل كل نشاط مهم في الـ API، مثل محاولات الـ login الفاشلة، والطلبات المشبوهة، والتغييرات في البيانات الحساسة. المشكلة أن الكثير من المطورين يعتمدون على الـ console.log فقط، وهذا غير كافٍ أبداً. يجب استخدام مكتبات متخصصة مثل Winston في Node.js أو Loguru في Python، ويجب تخزين الـ logs في مكان آمن وموزع مثل ELK Stack (Elasticsearch, Logstash, Kibana) أو Datadog.
لكن الـ Logging وحده لا يكفي. يجب أيضاً إعداد تنبيهات تلقائية عند اكتشاف نشاط مشبوه. مثلاً، إذا كان هناك ٥ محاولات تسجيل دخول فاشلة من نفس الـ IP خلال دقيقة واحدة، يجب إرسال تنبيه عبر البريد الإلكتروني أو Slack. أيضاً، يجب مراقبة الـ response times و الـ error rates. إذا لاحظت زيادة مفاجئة في الـ error rates، قد يكون ذلك إشارة لهجوم DDoS أو مشكلة في قاعدة البيانات. في شركات مثل GitHub، يستخدمون أنظمة مثل Prometheus و Grafana لمراقبة الأداء والأمان، مع إعداد تنبيهات فورية عند اكتشاف أي نشاط غير طبيعي.
// مثال على الـ Logging باستخدام Winston في Node.js
const winston = require('winston');
const { combine, timestamp, printf, colorize, json } = winston.format;
// إعداد الـ logger
const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
json()
),
transports: [
// تخزين الـ logs في ملف
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
// إرسال الـ logs إلى خدمة خارجية مثل Datadog أو ELK
new winston.transports.Http({
host: 'logs.example.com',
path: '/api/logs',
ssl: true
})
],
exceptionHandlers: [
new winston.transports.File({ filename: 'logs/exceptions.log' })
]
});
// استخدام الـ logger في الـ middleware لتسجيل الطلبات
app.use((req, res, next) => {
logger.info({
message: 'Incoming request',
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.id // إذا كان المستخدم مسجلاً دخول
});
next();
});
// تسجيل محاولة تسجيل دخول فاشلة
app.post('/login', (req, res) => {
const { email, password } = req.body;
// منطق التحقق من الـ credentials
if (!isValidCredentials(email, password)) {
logger.warn({
message: 'Failed login attempt',
email,
ip: req.ip,
userAgent: req.get('User-Agent')
});
return res.status(401).json({ error: 'Invalid credentials' });
}
// ...
});الفخ الشائع في الـ Logging هو تسجيل بيانات حساسة مثل كلمات المرور أو أرقام البطاقات الائتمانية. يجب دائماً تنظيف الـ logs من أي بيانات حساسة قبل تخزينها. أيضاً، يجب تحديد مدة الاحتفاظ بالـ logs، وعدم تخزينها إلى الأبد. في معظم الحالات، يكفي الاحتفاظ بالـ logs لمدة ٣٠ إلى ٩٠ يوماً، اعتماداً على متطلبات الأمان والامتثال. أيضاً، يجب حماية الـ logs نفسها من الوصول غير المصرح به. إذا تمكن المهاجم من الوصول إلى الـ logs، يمكنه الحصول على معلومات قيمة عن بنية النظام ونقاط الضعف المحتملة.
تأمين API ليس مهمة سهلة، لكنه أيضاً ليس مستحيلاً. المفتاح هو التفكير في كل طبقة من طبقات الأمان كخط دفاع مستقل، وعدم الاعتماد على طبقة واحدة فقط. ابدأ بالـ Authentication القوي باستخدام الـ JWT و الـ OAuth 2.0، ثم طبق الـ Authorization الدقيق باستخدام الـ RBAC أو الـ ABAC، ولا تنسَ الـ Input Validation و الـ Sanitization لمنع الهجمات الشائعة. بعد ذلك، ضع حدوداً صارمة للـ Rate Limiting لحماية الـ endpoints الحساسة، وأخيراً، قم بإعداد نظام قوي للـ Logging و الـ Monitoring لاكتشاف أي نشاط مشبوه قبل أن يتحول إلى كارثة.
الخطوة التالية لك هي مراجعة الـ API الخاص بك الآن، وتحديد الثغرات المحتملة. ابدأ باختبار الـ endpoints باستخدام أدوات مثل Postman أو Burp Suite، وحاول استغلال الثغرات بنفسك قبل أن يفعلها المهاجمون. إذا وجدت ثغرة، أصلحها فوراً ولا تنتظر. تذكر أن الـ security ليس مشروعاً جانبياً، بل هو جزء أساسي من تطوير أي تطبيق. إذا لم تكن متأكداً من كيفية تأمين جزء معين، استشر خبير أمني أو استخدم أدوات مثل OWASP ZAP لفحص الـ API تلقائياً. في النهاية، الـ security هو مسؤوليتك، وكل ثغرة تتركها مفتوحة هي فرصة للمهاجمين لاستغلالها.