الأمان في التطبيقات في الوقت الفعلي
الأمان أمر بالغ الأهمية في التطبيقات في الوقت الفعلي بسبب الاتصالات المستمرة والاتصال ثنائي الاتجاه. في هذا الدرس، سنغطي تدابير الأمان الشاملة لحماية تطبيقات WebSocket و Socket.io.
تهديدات أمان WebSocket
تواجه التطبيقات في الوقت الفعلي تحديات أمنية فريدة بما في ذلك الوصول غير المصرح به، والتلاعب بالرسائل، وهجمات رفض الخدمة، وحقن البيانات.
التهديدات الشائعة:
- اختطاف WebSocket عبر المواقع (CSWSH): مشابه لـ CSRF، يقوم المهاجمون بإنشاء اتصالات WebSocket غير مصرح بها
- حقن الرسائل: يرسل العملاء الضارون رسائل مصممة لاستغلال منطق الخادم
- هجمات DoS/DDoS: إرباك الخادم بطلبات الاتصال أو الرسائل
- الهجوم بالوسيط: اعتراض حركة مرور WebSocket غير المشفرة
- هجمات إعادة التشغيل: إعادة إرسال الرسائل الملتقطة لتنفيذ إجراءات غير مصرح بها
التحقق من الأصل
يمنع التحقق من الأصل النطاقات غير المصرح بها من الاتصال بخادم WebSocket الخاص بك.
// برمجية وسيطة للتحقق من الأصل
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);
io.use((socket, next) => {
const origin = socket.handshake.headers.origin;
if (!origin) {
return next(new Error('Origin header missing'));
}
if (!allowedOrigins.includes(origin)) {
console.warn(`رفض الاتصال من أصل غير مصرح به: ${origin}`);
return next(new Error('Unauthorized origin'));
}
next();
});
// التحقق من أصل خادم WebSocket
const wss = new WebSocket.Server({
verifyClient: (info, callback) => {
const origin = info.origin || info.req.headers.origin;
if (!allowedOrigins.includes(origin)) {
callback(false, 403, 'Forbidden origin');
return;
}
callback(true);
}
});
المصادقة والتفويض
نفذ مصادقة قوية لاتصالات WebSocket والتحقق من الأذونات لكل إجراء.
// برمجية وسيطة لمصادقة JWT
const jwt = require('jsonwebtoken');
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token ||
socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
throw new Error('Authentication token required');
}
// التحقق من JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// إرفاق معلومات المستخدم بالمقبس
socket.userId = decoded.userId;
socket.username = decoded.username;
socket.roles = decoded.roles || [];
// اختياري: التحقق من صحة الرمز في قاعدة البيانات
const isValid = await validateTokenInDB(token);
if (!isValid) {
throw new Error('Invalid or expired token');
}
next();
} catch (error) {
console.error('فشلت المصادقة:', error.message);
next(new Error('Authentication failed'));
}
});
// التفويض لإجراءات محددة
io.on('connection', (socket) => {
socket.on('admin:broadcast', async (data) => {
// التحقق مما إذا كان المستخدم لديه دور المسؤول
if (!socket.roles.includes('admin')) {
socket.emit('error', {
message: 'غير مصرح به: يتطلب وصول المسؤول'
});
return;
}
// المتابعة بإجراء المسؤول
io.emit('announcement', data);
});
socket.on('room:delete', async (roomId) => {
// التحقق مما إذا كان المستخدم يمتلك الغرفة
const room = await Room.findById(roomId);
if (room.ownerId !== socket.userId) {
socket.emit('error', {
message: 'غير مصرح به: يمكن لمالك الغرفة فقط الحذف'
});
return;
}
await room.delete();
io.to(roomId).emit('room:deleted');
});
});
تحديد معدل الاتصالات
يمنع تحديد المعدل الإساءة عن طريق تحديد عدد الاتصالات والرسائل من مصدر واحد.
// تحديد معدل الاتصال
const connectionAttempts = new Map();
io.use((socket, next) => {
const ip = socket.handshake.address;
const now = Date.now();
const windowMs = 60000; // دقيقة واحدة
const maxAttempts = 10;
// الحصول على أو إنشاء سجل المحاولة
let attempts = connectionAttempts.get(ip) || [];
// إزالة المحاولات القديمة
attempts = attempts.filter(time => now - time < windowMs);
// التحقق مما إذا تم تجاوز الحد
if (attempts.length >= maxAttempts) {
const resetTime = Math.ceil((attempts[0] + windowMs - now) / 1000);
return next(new Error(`عدد كبير جدًا من الاتصالات. حاول مرة أخرى في ${resetTime}s`));
}
// تسجيل هذه المحاولة
attempts.push(now);
connectionAttempts.set(ip, attempts);
next();
});
// تحديد معدل الرسائل لكل مقبس
const messageLimits = new Map();
function rateLimit(socket, eventName, maxMessages, windowMs) {
const key = `${socket.id}:${eventName}`;
const now = Date.now();
let messages = messageLimits.get(key) || [];
messages = messages.filter(time => now - time < windowMs);
if (messages.length >= maxMessages) {
socket.emit('error', {
code: 'RATE_LIMIT_EXCEEDED',
message: `عدد كبير جدًا من أحداث ${eventName}. يرجى التباطؤ.`
});
return false;
}
messages.push(now);
messageLimits.set(key, messages);
return true;
}
// الاستخدام
io.on('connection', (socket) => {
socket.on('chat:send', (data) => {
// السماح بـ 10 رسائل في الدقيقة
if (!rateLimit(socket, 'chat:send', 10, 60000)) {
return;
}
// معالجة الرسالة
io.emit('chat:message', data);
});
});
منع هجمات DoS
نفذ تدابير لمنع هجمات رفض الخدمة التي تستهدف البنية التحتية في الوقت الفعلي.
// حدود الاتصال القصوى
const MAX_CONNECTIONS_PER_USER = 5;
const MAX_TOTAL_CONNECTIONS = 10000;
const userConnections = new Map();
io.use((socket, next) => {
// التحقق من إجمالي الاتصالات
if (io.engine.clientsCount >= MAX_TOTAL_CONNECTIONS) {
return next(new Error('الخادم في طاقته القصوى'));
}
// التحقق من اتصالات المستخدم
const userId = socket.userId;
const userSockets = userConnections.get(userId) || [];
if (userSockets.length >= MAX_CONNECTIONS_PER_USER) {
return next(new Error('تم تجاوز الحد الأقصى للاتصالات لكل مستخدم'));
}
next();
});
io.on('connection', (socket) => {
// تتبع اتصالات المستخدم
const userId = socket.userId;
const userSockets = userConnections.get(userId) || [];
userSockets.push(socket.id);
userConnections.set(userId, userSockets);
socket.on('disconnect', () => {
const sockets = userConnections.get(userId) || [];
const index = sockets.indexOf(socket.id);
if (index > -1) {
sockets.splice(index, 1);
}
if (sockets.length === 0) {
userConnections.delete(userId);
} else {
userConnections.set(userId, sockets);
}
});
});
// حدود حجم الرسالة
const MAX_MESSAGE_SIZE = 10 * 1024; // 10KB
io.use((socket, next) => {
socket.use((packet, next) => {
const messageSize = JSON.stringify(packet).length;
if (messageSize > MAX_MESSAGE_SIZE) {
socket.emit('error', {
message: 'حجم الرسالة يتجاوز الحد الأقصى المسموح به'
});
return next(new Error('Message too large'));
}
next();
});
next();
});
أفضل ممارسة: نفذ التدهور الرشيق عند التعرض للهجوم. أعط الأولوية للمستخدمين المصادق عليهم، ونفذ أنظمة قوائم الانتظار، واستخدم CDN مع حماية DDoS لنقاط نهاية WebSocket.
التحقق من صحة الإدخال للرسائل
تحقق دائمًا من صحة الرسائل الواردة وقم بتعقيمها لمنع هجمات الحقن وتلف البيانات.
// التحقق الشامل من صحة الإدخال
const Joi = require('joi');
const validator = require('validator');
const xss = require('xss');
// التحقق من صحة المخطط
const chatMessageSchema = Joi.object({
message: Joi.string().min(1).max(1000).required(),
roomId: Joi.string().pattern(/^[a-zA-Z0-9-_]+$/).required(),
replyTo: Joi.string().optional()
});
io.on('connection', (socket) => {
socket.on('chat:send', async (data) => {
try {
// التحقق من صحة المخطط
const { error, value } = chatMessageSchema.validate(data);
if (error) {
socket.emit('error', {
message: 'تنسيق رسالة غير صالح',
details: error.details
});
return;
}
// تعقيم XSS
const sanitizedMessage = xss(value.message, {
whiteList: {}, // إزالة جميع HTML
stripIgnoreTag: true
});
// التحقق الإضافي
if (validator.isEmpty(sanitizedMessage.trim())) {
socket.emit('error', { message: 'رسالة فارغة' });
return;
}
// التحقق من أنماط البريد المزعج
if (isSpam(sanitizedMessage)) {
socket.emit('error', { message: 'تم اكتشاف بريد مزعج' });
logSuspiciousActivity(socket.userId, 'spam');
return;
}
// التحقق من عضوية الغرفة
const isMember = await verifyRoomMembership(
socket.userId,
value.roomId
);
if (!isMember) {
socket.emit('error', { message: 'ليس عضوًا في الغرفة' });
return;
}
// معالجة الرسالة الصالحة
const messageData = {
id: generateMessageId(),
userId: socket.userId,
username: socket.username,
message: sanitizedMessage,
roomId: value.roomId,
timestamp: Date.now()
};
await saveMessage(messageData);
io.to(value.roomId).emit('chat:message', messageData);
} catch (error) {
console.error('خطأ في معالجة الرسالة:', error);
socket.emit('error', { message: 'خطأ داخلي' });
}
});
});
function isSpam(message) {
const spamPatterns = [
/(?:https?:\/\/){2,}/i, // عدة عناوين URL
/(.)\1{10,}/, // أحرف متكررة
/\b(?:buy now|click here|free money)\b/i
];
return spamPatterns.some(pattern => pattern.test(message));
}
التشفير مع WSS (wss://)
استخدم دائمًا اتصالات WebSocket آمنة (wss://) في الإنتاج لتشفير البيانات أثناء النقل.
// إعداد WSS مع خادم HTTPS
const https = require('https');
const fs = require('fs');
const { Server } = require('socket.io');
const httpsServer = https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
ca: fs.readFileSync('/path/to/ca-bundle.pem') // اختياري
});
const io = new Server(httpsServer, {
cors: {
origin: 'https://myapp.com',
methods: ['GET', 'POST']
}
});
httpsServer.listen(3000, () => {
console.log('خادم WebSocket آمن يعمل على wss://localhost:3000');
});
// اتصال العميل
const socket = io('https://myapp.com', {
secure: true,
rejectUnauthorized: true, // التحقق من شهادة SSL
transports: ['websocket'] // استخدام WebSocket فقط
});
// تشفير إضافي للبيانات الحساسة
const crypto = require('crypto');
function encryptSensitiveData(data, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
function decryptSensitiveData(encryptedData, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
منع XSS في الدردشة
امنع هجمات البرمجة النصية عبر المواقع في تطبيقات الدردشة في الوقت الفعلي عن طريق تعقيم المحتوى الذي ينشئه المستخدم.
// منع XSS من جانب العميل
function displayMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
// استخدم textContent بدلاً من innerHTML
const textNode = document.createTextNode(message.content);
messageElement.appendChild(textNode);
// أو استخدم DOMPurify للمحتوى الغني
const clean = DOMPurify.sanitize(message.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
messageElement.innerHTML = clean;
document.getElementById('messages').appendChild(messageElement);
}
// التعقيم من جانب الخادم
const sanitizeHtml = require('sanitize-html');
socket.on('chat:send', (data) => {
const sanitized = sanitizeHtml(data.message, {
allowedTags: ['b', 'i', 'em', 'strong'],
allowedAttributes: {},
disallowedTagsMode: 'escape'
});
// فحوصات إضافية
if (containsMaliciousContent(sanitized)) {
logSecurityEvent(socket.userId, 'xss_attempt');
socket.emit('error', { message: 'تم اكتشاف محتوى غير صالح' });
return;
}
io.emit('chat:message', {
userId: socket.userId,
message: sanitized,
timestamp: Date.now()
});
});
قائمة التحقق من الأمان:
- ✓ استخدم WSS (wss://) في الإنتاج
- ✓ التحقق من رؤوس الأصل
- ✓ تنفيذ مصادقة JWT
- ✓ تحديد معدل الاتصالات والرسائل
- ✓ التحقق من صحة جميع المدخلات وتعقيمها
- ✓ تعيين حدود الاتصال وحجم الرسالة
- ✓ تسجيل الأحداث الأمنية للمراقبة
- ✓ تنفيذ CORS بشكل صحيح
- ✓ استخدام رؤوس الأمان (CSP، HSTS)
- ✓ عمليات تدقيق وتحديثات أمنية منتظمة
تمرين تطبيقي:
- نفذ نظام مصادقة كامل مع JWT لاتصالات Socket.io
- أنشئ برمجية وسيطة لتحديد المعدل تتتبع محاولات الاتصال وتكرار الرسائل
- ابنِ نظام التحقق من صحة الإدخال الذي يعقم رسائل الدردشة مع الحفاظ على التنسيق الآمن
- قم بإعداد WSS مع شهادات SSL المناسبة والتحقق من الأصل
- نفذ نظام تسجيل أمني يتتبع الأنشطة المشبوهة وينبه المسؤولين