API غير الآمن هو دعوة مفتوحة للهاكرز. في هذا الدليل العملي، نكشف لك كيف تبني دفاعات قوية من المصادقة المتعددة الطبقات إلى حماية الـ Rate Limiting، مع أمثلة كود حقيقية وحكايات من الخنادق البرمجية.
في آخر مشروع عملت عليه، تلقينا تنبيهاً من فريق الأمن بأن API الخاص بنا يستقبل ٣٠٠ طلب في الثانية من عنوان IP واحد. لم يكن الأمر هجوماً بالمعنى التقليدي، بل مجرد مطور نسي إزالة حلقة while(true) من كود الاختبار. النتيجة؟ السيرفر علق، قاعدة البيانات تجمدت، والـ Event Loop انهار تحت ضغط الـ I/O Bound. هذه الحادثة لم تكن الأولى ولن تكون الأخيرة، لكنها علمتني درساً قاسياً: الأمان ليس طبقة إضافية تضيفها بعد الانتهاء من التطوير، بل هو جزء لا يتجزأ من تصميم الـ API منذ اليوم الأول.
المشكلة الأكبر أن معظم المطورين يعتقدون أن وضع كلمة مرور على الـ API يكفي. الحقيقة هي أن الأمان يبدأ من كيفية تعامل السيرفر مع كل طلب، وكيفية تخزين الـ Tokens في الذاكرة، وحتى كيفية تفاعل الـ Middleware مع الـ Headers. في هذا المقال، سنفكك كل طبقة من طبقات الأمان، بدءاً من المصادقة وحتى حماية الـ Rate Limiting، مع التركيز على التفاصيل التي لا تذكرها الدروس التقليدية.
في بداية مشواري، كنت أستخدم Basic Auth في كل مشاريعي الصغيرة. الفكرة بسيطة: تضع اسم المستخدم وكلمة المرور في الـ Header وتشفّرها بـ Base64. المشكلة أن Base64 ليس تشفيراً حقيقياً، بل مجرد ترميز. أي شخص يستطيع اعتراض الـ Request (مثلاً عبر شبكة واي فاي عامة) يمكنه قراءة بيانات الاعتماد بسهولة تامة. حتى لو استخدمت HTTPS، فإن تخزين كلمة المرور في الكود أو في الـ Environment Variables ليس آمناً بما يكفي.
الحل؟ الانتقال إلى نظام الـ Tokens، لكن ليس أي tokens. مثلاً، JWT (JSON Web Tokens) أصبح معياراً شبه عالمي، لكنه يأتي مع مجموعة من الفخاخ. المشكلة الأولى أن الـ JWT لا يمكن إلغاؤه بسهولة. إذا سرق أحد الـ Token، فسيظل صالحاً حتى انتهاء صلاحيته. المشكلة الثانية أن الـ Payload في الـ JWT غير مشفر بشكل افتراضي، مما يعني أن أي شخص يستطيع قراءة البيانات المخزنة فيه. الحل؟ استخدام مكتبة مثل passport-jwt في Node.js مع استراتيجية مخصصة للتحقق من صلاحية الـ Token في كل طلب.
// مثال على استراتيجية JWT مع التحقق من القائمة السوداء
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
passReqToCallback: true // لتمرير الطلب الأصلي إلى الاستراتيجية
};
passport.use(new JwtStrategy(opts, async (req, jwt_payload, done) => {
// التحقق من القائمة السوداء للـ Tokens الملغاة
const isBlacklisted = await Blacklist.findOne({ token: req.headers.authorization.split(' ')[1] });
if (isBlacklisted) return done(null, false);
// التحقق من المستخدم
const user = await User.findById(jwt_payload.sub);
if (!user) return done(null, false);
return done(null, user);
}));OAuth 2.0 هو المعيار الذهبي للمصادقة في الوقت الحالي، لكنه ليس الحل السحري لكل المشاكل. الفكرة الأساسية وراء OAuth هي السماح للتطبيقات بالوصول إلى الموارد نيابة عن المستخدم دون الحاجة إلى مشاركة كلمة المرور. هذا رائع عندما تريد دمج خدمات خارجية مثل Google أو Facebook، لكنه يصبح كابوساً عندما تحاول تطبيقه داخلياً في نظام مغلق.
المشكلة الأكبر في OAuth 2.0 هي تعقيده. هناك أربعة أدوار رئيسية (Resource Owner, Client, Authorization Server, Resource Server) وثلاثة أنواع من الـ Tokens (Authorization Code, Access Token, Refresh Token). إذا لم تفهم جيداً كيف يتفاعل كل جزء مع الآخر، فستجد نفسك أمام ثغرات أمنية ضخمة. مثلاً، في عام ٢٠١٩، اكتشفت ثغرة في تطبيق شهير تسمح للمهاجمين بسرقة Access Tokens عبر استغلال تدفق الـ Implicit Flow، الذي أصبح الآن غير موصى به تماماً.
في تجربتي، أفضل استخدام OAuth 2.0 فقط عندما تحتاج إلى دمج خدمات خارجية. أما في الأنظمة الداخلية، فأفضل استخدام JWT مع Refresh Tokens مخزنة بشكل آمن في HttpOnly Cookies. هذا يقلل من تعقيد النظام ويوفر مستوى أمان مقبول دون الحاجة إلى إدارة سيرفرات تفويض معقدة.
# مثال على تنفيذ Refresh Tokens باستخدام Flask و JWT
from flask import Flask, request, jsonify
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity, set_access_cookies,
set_refresh_cookies, unset_jwt_cookies
)
import datetime
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'super-secret-key' # يجب أن تكون في Environment Variables
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(minutes=15)
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = datetime.timedelta(days=30)
jwt = JWTManager(app)
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username', None)
password = request.json.get('password', None)
# التحقق من صحة بيانات الاعتماد (يجب استخدام Hashing لكلمة المرور)
if username != 'admin' or password != 'password':
return jsonify({'msg': 'Bad credentials'}), 401
# إنشاء Tokens
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
resp jsonify({'msg': 'Login successful'})
set_access_cookies(response, access_token)
set_refresh_cookies(response, refresh_token)
return response
@app.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user)
response = jsonify({'msg': 'Token refreshed'})
set_access_cookies(response, new_token)
return response
@app.route('/logout', methods=['POST'])
def logout():
response = jsonify({'msg': 'Logout successful'})
unset_jwt_cookies(response)
return responseفي أحد المشاريع التي عملت عليها، تلقينا هجوماً من نوع DDoS استهدف الـ API الخاص بنا. المهاجمون استخدموا شبكة من الـ Bots لإرسال آلاف الطلبات في الثانية، مما أدى إلى تحميل زائد على السيرفر وقاعدة البيانات. الحل؟ تطبيق نظام Rate Limiting ذكي لا يعتمد فقط على عدد الطلبات، بل أيضاً على نوع المستخدم ونوع الطلب.
الـ Rate Limiting ليس مجرد حد أقصى لعدد الطلبات في الدقيقة. يجب أن يكون نظاماً متعدد الطبقات يأخذ في الاعتبار عدة عوامل: عنوان IP، نوع المستخدم (مجاني مقابل مدفوع)، نوع الـ Endpoint (GET مقابل POST)، وحتى التوقيت. مثلاً، يمكنك السماح بـ ١٠٠ طلب في الدقيقة للمستخدمين المجانيين، و١٠٠٠ طلب للمستخدمين المدفوعين، مع زيادة مؤقتة في الحد الأقصى أثناء الحملات الترويجية.
أفضل مكتبة لتطبيق Rate Limiting في Node.js هي rate-limiter-flexible، التي تدعم عدة استراتيجيات مثل Fixed Window و Sliding Window و Token Bucket. المشكلة مع Fixed Window أنها تسمح بتجاوز الحد الأقصى في بعض الحالات (مثلاً، إذا أرسل المستخدم ٩٩ طلباً في الثانية الأخيرة من النافذة، ثم ٩٩ طلباً في الثانية الأولى من النافذة الجديدة). الحل؟ استخدام Sliding Window، التي تحتسب الطلبات في آخر ٦٠ ثانية بشكل مستمر.
// مثال على Rate Limiting باستخدام Sliding Window
const { RateLimiterMemory } = require('rate-limiter-flexible');
const opts = {
points: 100, // عدد الطلبات المسموح بها
duration: 60, // النافذة الزمنية بالثواني
};
const rateLimiter = new RateLimiterMemory(opts);
app.use(async (req, res, next) => {
try {
const userId = req.user ? req.user.id : req.ip; // استخدام IP للمستخدمين غير المسجلين
const rateLimitRes = await rateLimiter.consume(userId);
// إضافة Headers لإعلام العميل بالحدود المتبقية
res.set({
'X-RateLimit-Limit': opts.points,
'X-RateLimit-Remaining': rateLimitRes.remainingPoints,
'X-RateLimit-Reset': new Date(Date.now() + rateLimitRes.msBeforeNext)
});
next();
} catch (rejRes) {
res.status(429).json({
error: 'Too Many Requests',
retryAfter: rejRes.msBeforeNext / 1000
});
}
});إذا كان تطبيقك يعمل على سيرفر واحد، فإن استخدام الذاكرة المحلية لـ Rate Limiting قد يكون كافياً. لكن إذا كان لديك عدة سيرفرات خلف موازن حمل (Load Balancer)، فإن الذاكرة المحلية لن تكون كافية، لأن كل سيرفر سيكون له عداد منفصل. الحل؟ استخدام Redis، الذي يوفر ذاكرة مشتركة بين جميع السيرفرات.
المشكلة مع Redis هي أنه يضيف تعقيداً إضافياً للنظام. يجب أن تتأكد من أن الاتصال بـ Redis آمن وموثوق، وأنك تتعامل مع حالات الفشل بشكل صحيح. مثلاً، إذا تعطل اتصال Redis، يجب أن تقرر ما إذا كنت ستسمح بالطلبات (خطر تجاوز الحد الأقصى) أو ترفضها (خطر رفض المستخدمين الشرعيين). في معظم الحالات، أفضل استخدام استراتيجية Fail-Open، أي السماح بالطلبات إذا تعطل Redis، مع تسجيل الحدث لتحليله لاحقاً.
// مثال على Rate Limiting باستخدام Redis
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redis = require('redis');
const redisClient = redis.createClient({
host: 'localhost',
port: 6379,
enable_offline_queue: false, // لمنع التراكم عند تعطل Redis
});
const opts = {
storeClient: redisClient,
points: 100,
duration: 60,
blockDuration: 60, // حظر المستخدم لمدة 60 ثانية إذا تجاوز الحد
};
const rateLimiter = new RateLimiterRedis(opts);
app.use(async (req, res, next) => {
try {
const userId = req.user ? req.user.id : req.ip;
const rateLimitRes = await rateLimiter.consume(userId);
res.set({
'X-RateLimit-Limit': opts.points,
'X-RateLimit-Remaining': rateLimitRes.remainingPoints,
});
next();
} catch (rejRes) {
res.status(429).json({
error: 'Too Many Requests',
retryAfter: rejRes.msBeforeNext / 1000
});
}
});في عام ٢٠١٨، اكتشف باحث أمني ثغرة في تطبيق شهير تسمح للمهاجمين بسرقة بيانات المستخدمين عبر هجوم CSRF. المشكلة كانت في أن التطبيق لم يستخدم الـ CSRF Tokens في طلبات الـ POST، مما سمح للمهاجمين بإرسال طلبات نيابة عن المستخدمين دون علمهم. هذه الثغرة ليست جديدة، لكنها لا تزال شائعة لأن الكثير من المطورين لا يفهمون الفرق بين CORS و CSRF.
CORS (Cross-Origin Resource Sharing) هو آلية تسمح للمواقع بالوصول إلى الموارد من نطاقات مختلفة. المشكلة أن الكثير من المطورين يضبطون سياسة CORS بشكل فضفاض جداً، مثل السماح بكل النطاقات (*). هذا يسمح لأي موقع بالوصول إلى الـ API الخاص بك، مما يجعله عرضة لهجمات مثل CSRF و XSS. الحل؟ تحديد النطاقات المسموح بها بدقة، وعدم السماح أبداً بطلبات غير آمنة (مثل PUT و DELETE) من نطاقات غير موثوقة.
أما CSRF (Cross-Site Request Forgery) فهو هجوم يسمح للمهاجم بإرسال طلبات نيابة عن المستخدم دون علمه. الحل التقليدي هو استخدام CSRF Tokens، لكن هذا يضيف تعقيداً إضافياً. في التطبيقات الحديثة، أفضل استخدام SameSite Cookies، التي تمنع المتصفح من إرسال الـ Cookies في الطلبات من نطاقات مختلفة. هذا يحمي من معظم هجمات CSRF دون الحاجة إلى إدارة Tokens إضافية.
// مثال على ضبط سياسة CORS بشكل آمن
const cors = require('cors');
const corsOpti {
origin: ['https://yourdomain.com', 'https://api.yourdomain.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // يسمح بإرسال Cookies
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
// مثال على استخدام SameSite Cookies
app.use((req, res, next) => {
res.cookie('sessionId', '12345', {
httpOnly: true,
secure: true, // يتطلب HTTPS
sameSite: 'strict', // أو 'lax' للسماح ببعض الطلبات من نطاقات أخرى
maxAge: 1000 * 60 * 60 * 24 // يوم واحد
});
next();
});HTTPS أصبح معياراً أساسياً لأي موقع أو API في الوقت الحالي. المشكلة أن الكثير من المطورين يعتقدون أن HTTPS يكفي لحماية البيانات. الحقيقة هي أن HTTPS يحمي البيانات أثناء النقل فقط، لكنه لا يحميها أثناء التخزين أو المعالجة. مثلاً، إذا كنت تخزن بيانات حساسة في قاعدة البيانات دون تشفير، فإن اختراق قاعدة البيانات يعني أن المهاجم يستطيع قراءة كل شيء.
الحل؟ استخدام التشفير من طرف إلى طرف (End-to-End Encryption). مثلاً، في تطبيقات المراسلة مثل Signal، يتم تشفير الرسائل على جهاز المرسل وفك تشفيرها على جهاز المستقبل فقط. هذا يعني أن حتى إذا تم اختراق السيرفر، فإن المهاجم لن يستطيع قراءة الرسائل. لكن هذا يضيف تعقيداً كبيراً، خاصة في التطبيقات التي تحتاج إلى معالجة البيانات على السيرفر (مثل البحث أو التحليل).
في معظم الحالات، يكفي استخدام HTTPS مع تشفير البيانات الحساسة في قاعدة البيانات. مثلاً، يمكنك استخدام مكتبة مثل bcrypt لتشفير كلمات المرور، و AES لتشفير البيانات الأخرى. لكن يجب أن تتأكد من أن مفاتيح التشفير مخزنة بشكل آمن، ويفضل أن تكون في خدمة إدارة مفاتيح مثل AWS KMS أو HashiCorp Vault.
# مثال على تشفير البيانات باستخدام AES
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import base64
# توليد مفتاح سري عشوائي
key = get_random_bytes(32) # 256-bit key
# تشفير البيانات
def encrypt_data(data, key):
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(data.encode('utf-8'))
return {
'ciphertext': base64.b64encode(ciphertext).decode('utf-8'),
'nonce': base64.b64encode(cipher.nonce).decode('utf-8'),
'tag': base64.b64encode(tag).decode('utf-8')
}
# فك تشفير البيانات
def decrypt_data(encrypted_data, key):
cipher = AES.new(
key,
AES.MODE_GCM,
nbase64.b64decode(encrypted_data['nonce'])
)
data = cipher.decrypt_and_verify(
base64.b64decode(encrypted_data['ciphertext']),
base64.b64decode(encrypted_data['tag'])
)
return data.decode('utf-8')
# مثال على الاستخدام
encrypted = encrypt_data('بيانات حساسة', key)
print(encrypted)
decrypted = decrypt_data(encrypted, key)
print(decrypted)بعد أكثر من عشر سنوات في تطوير الـ APIs، تعلمت أن الأمان ليس مجرد طبقة تضاف في النهاية، بل هو عقلية يجب أن تتغلغل في كل سطر من الكود. إليك بعض النصائح التي لن تجدها في الدروس التقليدية:
في النهاية، الأمان ليس وجهة، بل رحلة مستمرة. كل يوم تظهر تهديدات جديدة، وكل يوم يجب أن تتعلم شيئاً جديداً. لكن إذا بنيت دفاعات قوية منذ البداية، فستكون مستعداً لأي شيء يأتي في طريقك.