API غير المؤمنة هي بوابة مفتوحة للهاكرز. في هذا الدليل العملي، سنفكك كل طبقة أمنية من المصادقة حتى الحد من الطلبات، مع أكواد حقيقية وحيل من عالم الإنتاج لتجنب الثغرات التي لا يتحدث عنها أحد.
في أحد أيام الجمعة العادية، تلقى فريق DevOps في شركة ناشئة رسالة من AWS تبلغهم أن فاتورة الخادم قفزت من ٤٠٠ دولار إلى ١٢ ألف دولار خلال ليلة واحدة. السبب؟ هجوم DDoS استهدف API الخاص بهم، ولم يكن هناك أي حد للطلبات. ٥٠٠ طلب في الثانية الواحدة حولت السيرفر إلى فرن مشتعل. هذه ليست قصة درامية، بل واقع يومي لمطورين نسوا أن الأمان ليس خطوة أخيرة تضاف قبل الإطلاق، بل هو جزء لا يتجزأ من تصميم API منذ اليوم الأول. لنبدأ من حيث يبدأ الخطأ: المصادقة.
Basic Auth هو أسوأ قرار أمني يمكن أن تتخذه لمشروعك. نعم، هو سهل التنفيذ - تضع اسم مستخدم وكلمة مرور في الـ header وتقول للسيرفر: "صدقني، أنا المطور". لكن المشكلة أن هذه البيانات تُرسل ك Base64، وهو تشفير وليس تشفيراً. أي شخص يستطيع التقاط الـ packets باستخدام Wireshark سيقرأ بياناتك كما يقرأ رسالة نصية. في عام ٢٠٢٢، اكتشف باحثون أن ٣٨٪ من APIs التي تستخدم Basic Auth تُخترق خلال أول شهر من إطلاقها لأن المطورين ينسون تغيير كلمات المرور الافتراضية أو يستخدمون كلمات ضعيفة مثل "admin:1234".
الحل الوحيد المقبول هو JWT (JSON Web Tokens) أو OAuth 2.0. لكن حتى هنا، هناك فخاخ. مثلاً، الكثير من المطورين يضعون معلومات حساسة داخل الـ payload الخاص بالـ JWT ظناً منهم أنه آمن لأنه موقّع. الحقيقة أن الـ payload يُقرأ بسهولة إذا لم يُشفّر. في إحدى المرات، وجدت فريقاً يضع رقم بطاقة الائتمان داخل الـ JWT بدون تشفير، وعندما سُرق الـ token، كانت البيانات مكشوفة تماماً. القاعدة الذهبية: لا تضع في الـ JWT سوى معرف المستخدم (user ID) ودور المستخدم (role)، وكل شيء آخر يجب أن يُجلب من قاعدة البيانات باستخدام هذا المعرف.
// مثال على توليد JWT آمن في Node.js
const jwt = require('jsonwebtoken');
const secretKey = process.env.JWT_SECRET; // يجب أن يكون طولها 32 حرفاً على الأقل
function generateToken(user) {
return jwt.sign(
{
id: user.id,
role: user.role // لا تضع بيانات حساسة هنا
},
secretKey,
{ expiresIn: '1h' } // مدة صلاحية قصيرة تقلل من خطر السرقة
);
}
// التحقق من الـ token
function verifyToken(token) {
try {
const decoded = jwt.verify(token, secretKey);
return decoded;
} catch (err) {
throw new Error('Invalid or expired token');
}
}لكن حتى مع JWT، هناك مشكلة أخرى: الـ token يُسرق. إذا استطاع المهاجم الحصول على الـ token، يمكنه انتحال شخصية المستخدم حتى تنتهي صلاحيته. هنا يأتي دور الـ refresh tokens. الفكرة بسيطة: بدلاً من إعطاء المستخدم token واحد صالح لساعات، نعطيه token قصير الأجل (مثلاً ساعة) وrefresh token طويل الأجل (مثلاً أسبوع). عندما تنتهي صلاحية الـ token القصير، يرسل العميل الـ refresh token للحصول على token جديد. بهذه الطريقة، حتى لو سُرق الـ token القصير، فلن يكون صالحاً لأكثر من ساعة. لكن انتبه: الـ refresh token يجب أن يُخزن في قاعدة بيانات ولا يُرسل أبداً إلى العميل بشكل مباشر، ويجب أن يُحذف فوراً إذا تم استخدامه للحصول على token جديد.
المصادقة تخبرك من هو المستخدم، لكن التفويض يخبرك ماذا يستطيع أن يفعل. الكثير من APIs تتوقف عند المصادقة وتستخدم أدواراً بسيطة مثل "admin" و"user"، وهذا خطأ فادح. في عام ٢٠٢١، اكتشف باحثون ثغرة في API لشركة شهيرة تسمح لأي مستخدم عادي بحذف حسابات المستخدمين الآخرين ببساطة عن طريق تغيير الـ user ID في الـ request. السبب؟ لم يكن هناك تحقق من أن المستخدم الذي يرسل الطلب يملك الصلاحية للقيام بهذا الإجراء.
الحل هو استخدام RBAC (Role-Based Access Control) أو حتى الأفضل، ABAC (Attribute-Based Access Control). في RBAC، يكون لكل دور مجموعة من الصلاحيات المحددة مسبقاً. مثلاً، دور "editor" يستطيع تعديل المقالات، لكن لا يستطيع حذفها. أما في ABAC، فالأمور أكثر ديناميكية: بدلاً من الاعتماد على أدوار ثابتة، تتخذ القرارات بناءً على سمات المستخدم والسياق. مثلاً، يمكن السماح للمستخدم بتعديل المقالة فقط إذا كان هو كاتبها أو إذا كان المدير ومسجل الدخول من شبكة الشركة الداخلية. هذا النوع من التحكم يتطلب المزيد من الجهد في البداية، لكنه يمنع الكثير من الثغرات لاحقاً.
# مثال على ABAC في Flask
from flask import request, jsonify
from functools import wraps
def check_permission(resource, action):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
user = get_current_user() # تحصل على المستخدم الحالي من الـ token
# قواعد ABAC: المستخدم يستطيع تعديل المقالة إذا كان كاتبها
# أو إذا كان مدير ومسجل من شبكة داخلية
if action == 'update' and resource == 'article':
article = get_article(kwargs['article_id'])
if user.id == article.author_id:
return f(*args, **kwargs)
if user.role == 'admin' and is_internal_network(request.remote_addr):
return f(*args, **kwargs)
return jsonify({'error': 'Unauthorized'}), 403
return f(*args, **kwargs)
return wrapper
return decorator
# استخدام الديكوريتور
@app.route('/articles/<int:article_id>', methods=['PUT'])
@check_permission('article', 'update')
def update_article(article_id):
# منطق تعديل المقالة
return jsonify({'message': 'Article updated'})لكن حتى مع ABAC، هناك مشكلة شائعة: الـ privilege escalation. مثلاً، إذا كان هناك API endpoint يسمح للمستخدم بتحديث دوره الخاص، قد يحاول مستخدم عادي تغيير دوره إلى "admin". الحل هو التأكد من أن أي تغيير في الصلاحيات يتطلب دوراً أعلى من الدور المطلوب. مثلاً، لا تسمح لأي مستخدم بتغيير دوره الخاص، بل اجعل هذا الإجراء مقتصراً على المستخدمين الذين لديهم دور "superadmin" بالفعل. أيضاً، استخدم قاعدة بيانات منفصلة لتخزين الصلاحيات ولا تعتمد على الـ JWT فقط، لأن الـ token قد يكون قد سُرق أو انتهت صلاحيته.
هجمات الحقن هي ثاني أخطر ثغرة في APIs بعد المصادقة الضعيفة، وفقاً لتقرير OWASP API Security Top 10. الفكرة بسيطة: المهاجم يرسل بيانات مصممة خصيصاً لتخريب منطق التطبيق. أشهر مثال هو SQL Injection، حيث يرسل المهاجم استعلام SQL ضار داخل الـ input ليتم تنفيذه على قاعدة البيانات. لكن الحقن لا يقتصر على SQL فقط - هناك أيضاً NoSQL Injection، Command Injection، وحتى XPath Injection إذا كنت تستخدم XML.
في إحدى المرات، كنت أعمل على API يستخدم MongoDB، وفوجئت بأن أحد الـ endpoints يسمح بإدخال استعلام كامل من العميل. مثلاً، كان يمكن للمهاجم إرسال طلب مثل هذا: {"username": {"$ne": ""}, "password": {"$ne": ""}} وهذا الاستعلام يرجع جميع المستخدمين الذين لديهم أي اسم مستخدم وأي كلمة مرور، مما يسمح بتسجيل الدخول دون معرفة أي بيانات. السبب؟ المطور استخدم الـ query كما هو دون أي فلترة. الحل هو استخدام الـ parameterized queries دائماً، سواء كنت تستخدم SQL أو NoSQL.
// مثال على حماية من NoSQL Injection في Node.js مع Mongoose
const User = require('./models/User');
// ❌ خطير: يسمح بحقن الاستعلامات
app.get('/users', async (req, res) => {
const users = await User.find(req.query); // يمكن للمهاجم إرسال أي استعلام
res.json(users);
});
// ✅ آمن: استخدام parameterized queries
app.get('/users', async (req, res) => {
// نحدد الحقول المسموح بها فقط
const allowedFields = ['name', 'email', 'role'];
const query = {};
for (const field of allowedFields) {
if (req.query[field]) {
query[field] = req.query[field];
}
}
const users = await User.find(query);
res.json(users);
});لكن الحماية من الحقن لا تتوقف عند قاعدة البيانات. إذا كنت تستخدم أوامر النظام مثل exec أو spawn في Node.js، يجب أن تكون حذراً جداً. مثلاً، إذا كان لديك API يسمح بتشغيل أوامر على السيرفر بناءً على مدخلات المستخدم، قد يحاول المهاجم إرسال أمر مثل rm -rf / ليحذف ملفات النظام. الحل هو استخدام القوائم البيضاء (whitelists) دائماً. مثلاً، إذا كان الـ API يسمح بتشغيل أوامر محددة فقط مثل git pull أو npm install، يجب أن تتحقق من أن الأمر المطلوب موجود في قائمة الأوامر المسموح بها قبل تنفيذه.
في عام ٢٠٢٠، تعرضت شركة Cloudflare لهجوم DDoS وصل إلى ١.٧ تيرابايت في الثانية. الهجوم لم يستهدف موقعهم الرئيسي، بل استهدف API واحد غير محمي بـ Rate Limiting. النتيجة؟ السيرفرات توقفت عن الاستجابة، والخدمة تعطلت لساعات. Rate Limiting ليس مجرد ميزة إضافية، بل هو ضرورة لمنع استنزاف موارد السيرفر سواء كان الهجوم متعمداً أو ناتجاً عن خطأ برمجي مثل loop لا نهائي يرسل طلبات متكررة.
الفكرة الأساسية لـ Rate Limiting هي تحديد عدد الطلبات التي يمكن للمستخدم أو الـ IP إرسالها خلال فترة زمنية محددة. مثلاً، يمكنك السماح بـ ١٠٠ طلب في الدقيقة لكل مستخدم. لكن هنا تأتي المشكلة: كيف تحدد من هو "المستخدم"؟ إذا كنت تعتمد على الـ IP فقط، قد تواجه مشكلة مع المستخدمين الذين يستخدمون شبكات مشتركة مثل الجامعات أو مقاهي الإنترنت، حيث قد يتشارك مئات المستخدمين نفس الـ IP. الحل هو استخدام الـ API key أو الـ user ID بالإضافة إلى الـ IP لتحديد الهوية.
# Rate Limiting باستخدام Redis في Flask
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
# إعداد Redis لتخزين عدد الطلبات
limiter = Limiter(
app,
key_func=lambda: f"{get_remote_address()}:{request.headers.get('X-API-KEY', '')}",
storage_uri="redis://localhost:6379",
strategy="fixed-window" # أو "moving-window" للحصول على دقة أفضل
)
# تطبيق Rate Limiting على جميع الـ endpoints
@app.before_request
def before_request():
# السماح بـ 100 طلب في الدقيقة لكل مستخدم
limiter.limit("100 per minute")(lambda: None)()
@app.route('/api/data')
def get_data():
return jsonify({'data': 'Some data'})لكن Rate Limiting له تحدياته أيضاً. مثلاً، إذا كان لديك API يستخدمه تطبيق جوال، قد يكون هناك عشرات الآلاف من المستخدمين الذين يرسلون طلبات في نفس الوقت. هنا، يجب أن تفكر في تقسيم الـ Rate Limits بناءً على نوع المستخدم. مثلاً، المستخدمين العاديين يسمح لهم بـ ١٠٠ طلب في الدقيقة، بينما الـ bots يسمح لهم بـ ١٠ طلبات فقط. أيضاً، يجب أن تفكر في الـ burst requests - طلبات تأتي دفعة واحدة بعد فترة هدوء. مثلاً، إذا كان المستخدم لم يرسل أي طلبات لمدة ساعة، ثم أرسل ٥٠ طلباً في ثانية واحدة، هل يجب رفضها كلها؟ الحل هو استخدام خوارزميات مثل Token Bucket أو Leaky Bucket التي تسمح ببعض الـ burst مع الحفاظ على الحد الإجمالي.
HTTPS يحمي البيانات أثناء النقل، لكنه لا يحميها أثناء التخزين أو المعالجة. في عام ٢٠١٩، اكتشف باحثون أن شركة شهيرة تخزن كلمات مرور المستخدمين بدون تشفير في قاعدة البيانات. عندما سُرقت قاعدة البيانات، كانت كلمات المرور مكشوفة تماماً. التشفير يجب أن يكون على عدة مستويات: أثناء النقل (HTTPS)، أثناء التخزين (تشفير قاعدة البيانات)، وحتى أثناء المعالجة إذا كانت البيانات حساسة للغاية.
أولاً، تأكد من أن HTTPS ليس اختيارياً. استخدم HSTS (HTTP Strict Transport Security) لإجبار المتصفح على استخدام HTTPS دائماً. ثانياً، إذا كنت تخزن بيانات حساسة مثل كلمات المرور أو أرقام البطاقات الائتمانية، استخدم تشفير قوي مثل AES-256 للتشفير أثناء التخزين. لكن هنا تأتي المشكلة: أين تخزن مفتاح التشفير؟ إذا خزنت المفتاح في نفس قاعدة البيانات، فسيكون مثل وضع القفل والمفتاح في نفس المكان. الحل هو استخدام خدمات مثل AWS KMS أو HashiCorp Vault لتخزين المفاتيح بشكل آمن.
// تشفير البيانات باستخدام AES-256 في Node.js
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32); // يجب تخزين هذا المفتاح في مكان آمن مثل AWS KMS
const iv = crypto.randomBytes(16); // متجه التهيئة
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return { iv: iv.toString('hex'), encryptedData: encrypted };
}
function decrypt(encryptedData) {
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// مثال على الاستخدام
const encrypted = encrypt('Sensitive Data');
console.log('Encrypted:', encrypted);
console.log('Decrypted:', decrypt(encrypted));لكن التشفير له تحدياته أيضاً. مثلاً، إذا كنت تستخدم تشفير البيانات في قاعدة البيانات، فستفقد القدرة على البحث والاستعلام عن هذه البيانات بسهولة. الحل هو استخدام تقنيات مثل التشفير القابل للبحث (Searchable Encryption) أو تخزين الـ hash للقيم بدلاً من القيم الأصلية. مثلاً، إذا كنت تريد التحقق من كلمة مرور المستخدم، لا تخزن كلمة المرور نفسها، بل خزن الـ hash باستخدام خوارزمية قوية مثل bcrypt أو Argon2. بهذه الطريقة، حتى إذا سُرقت قاعدة البيانات، لن يستطيع المهاجم الحصول على كلمات المرور الأصلية.
في عام ٢٠١٧، تعرضت شركة Equifax لاختراق أدى إلى سرقة بيانات ١٤٧ مليون مستخدم. التحقيقات كشفت أن الثغرة كانت معروفة منذ أشهر، لكن الشركة لم تكتشف الهجوم إلا بعد فوات الأوان. السبب؟ لم يكن هناك نظام مراقبة فعال. المراقبة والتسجيل ليسا مجرد أدوات لتصحيح الأخطاء، بل هما خط الدفاع الأول ضد الهجمات. إذا لم تراقب نشاط API الخاص بك، فأنت لا تعرف ما إذا كان هناك هجوم جارٍ أم لا.
أولاً، يجب أن تسجل كل طلب يأتي إلى API الخاص بك. لكن ليس أي تسجيل - يجب أن يكون التسجيل ذكياً. مثلاً، سجل الـ IP، الـ user agent، الـ endpoint المطلوب، الوقت، وحالة الاستجابة (success أو error). لكن انتبه: لا تسجل بيانات حساسة مثل كلمات المرور أو أرقام البطاقات الائتمانية، لأن ذلك قد ينتهك قوانين الخصوصية مثل GDPR. ثانياً، استخدم أدوات مثل ELK Stack (Elasticsearch, Logstash, Kibana) أو Grafana لتحليل السجلات في الوقت الفعلي. مثلاً، إذا لاحظت أن هناك ١٠٠٠ طلب فاشل من نفس الـ IP خلال دقيقة واحدة، قد يكون ذلك محاولة brute force ويجب حظر هذا الـ IP تلقائياً.
# مثال على إعداد Filebeat لإرسال السجلات إلى Elasticsearch
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/api/*.log
json.keys_under_root: true
json.add_error_key: true
output.elasticsearch:
hosts: ["localhost:9200"]
indices:
- index: "api-logs-%{+yyyy.MM.dd}"
# إعداد Kibana لتحليل السجلات
# يمكنك إنشاء dashboard يظهر عدد الطلبات في الدقيقة، نسبة الأخطاء، وأكثرثالثاً، استخدم أدوات مثل OWASP ZAP أو Burp Suite لفحص API الخاص بك بانتظام بحثاً عن الثغرات. هذه الأدوات تستطيع اكتشاف ثغرات مثل SQL Injection، XSS، و CSRF قبل أن يستغلها المهاجمون. أيضاً، فكر في استخدام خدمات مثل AWS WAF أو Cloudflare لحماية API من الهجمات الشائعة. مثلاً، AWS WAF يسمح لك بإنشاء قواعد تمنع الطلبات التي تحتوي على أنماط معينة مثل SQL Injection أو XSS.
الأمان ليس طبقة واحدة تضاف في النهاية، بل هو سلسلة من الطبقات التي تبدأ من المصادقة وتنتهي بالمراقبة. إذا تركت ثغرة واحدة مفتوحة، فستكون بوابة لدخول المهاجمين. القاعدة الأساسية هي: لا تثق بأي شيء يأتي من الخارج، سواء كان ذلك بيانات المستخدم أو حتى الـ headers. استخدم JWT للمصادقة، ABAC للتفويض، parameterized queries للحماية من الحقن، Rate Limiting لمنع استنزاف الموارد، التشفير لحماية البيانات، والمراقبة لاكتشاف الهجمات قبل أن تتفاقم. وإذا كان هناك شيء واحد يجب أن تأخذه من هذا المقال، فهو هذا: الأمان ليس ترفاً، بل هو مسؤولية. إذا لم تؤمن API الخاص بك، فأنت لا تخاطر بمشروعك فقط، بل تخاطر ببيانات المستخدمين أيضاً.