تطبيقات الويب التقدمية
أمان تطبيقات الويب التقدمية
أمان تطبيقات الويب التقدمية
الأمان أمر بالغ الأهمية في تطبيقات الويب التقدمية. يغطي هذا الدرس ممارسات الأمان الأساسية بما في ذلك فرض HTTPS، وسياسة أمان المحتوى، وأمان عامل الخدمة، والحماية من ثغرات الويب الشائعة.
فرض HTTPS
HTTPS إلزامي لتطبيقات الويب التقدمية. عمال الخدمة، والإشعارات الفورية، والعديد من واجهات برمجة التطبيقات الحديثة تعمل فقط عبر اتصالات آمنة.
<!-- إعادة توجيه HTTP إلى HTTPS -->
<script>
// فرض إعادة توجيه HTTPS
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost') {
window.location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
}
// التحقق من السياق الآمن
if (window.isSecureContext) {
console.log('يعمل في سياق آمن (HTTPS)');
// تهيئة ميزات PWA
initServiceWorker();
} else {
console.warn('ليس في سياق آمن. ميزات PWA غير متاحة.');
}
</script>
// فرض HTTPS من جانب الخادم (Node.js/Express)
const express = require('express');
const app = express();
// وسيط فرض HTTPS
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
// HSTS (أمان النقل الصارم HTTP)
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
next();
});
ملاحظة: HSTS (أمان النقل الصارم HTTP) يخبر المتصفحات باستخدام HTTPS دائمًا لنطاقك. توجيه preload يسمح بالإرسال إلى قائمة التحميل المسبق لـ HSTS.
سياسة أمان المحتوى (CSP)
تمنع CSP هجمات XSS من خلال التحكم في الموارد التي يمكن تحميلها وتنفيذها على صفحتك.
<!-- CSP عبر علامة meta -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self' https://fonts.googleapis.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;">
// رؤوس CSP من جانب الخادم (Node.js/Express)
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"], // تجنب 'unsafe-inline' إن أمكن
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.googleapis.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: []
}
}));
// إبلاغ عن انتهاكات CSP
app.use(helmet.contentSecurityPolicy({
directives: {
// ... توجيهات أخرى
reportUri: '/csp-violation-report'
}
}));
app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('انتهاك CSP:', req.body);
// تسجيل في نظام مراقبة الأمان
res.status(204).end();
});
تحذير: تجنب استخدام 'unsafe-inline' و'unsafe-eval' في CSP. إنها تضعف الأمان بشكل كبير. استخدم nonces أو hashes للنصوص البرمجية المضمنة بدلاً من ذلك.
أمان عامل الخدمة
عمال الخدمة لديهم قدرات قوية ويجب تأمينها بشكل صحيح.
// service-worker.js - ممارسات التخزين المؤقت الآمنة
const CACHE_NAME = 'pwa-v1';
const ALLOWED_ORIGINS = [
'https://your-domain.com',
'https://api.your-domain.com'
];
// تخزين مؤقت فقط لطلبات نفس المصدر أو المصادر المسموحة عبر المصدر
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// فحوصات الأمان
if (!ALLOWED_ORIGINS.includes(url.origin) && url.origin !== self.location.origin) {
// لا تخزن موارد الطرف الثالث مؤقتًا
return;
}
// لا تخزن الطلبات الحساسة مؤقتًا
if (url.pathname.includes('/api/auth') ||
url.pathname.includes('/api/payment')) {
return;
}
// التحقق من صحة طريقة الطلب
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((response) => {
// لا تخزن ردود الأخطاء مؤقتًا
if (!response || response.status !== 200) {
return response;
}
// استنساخ الرد للتخزين المؤقت
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// معالجة رسالة آمنة
self.addEventListener('message', (event) => {
// التحقق من أصل الرسالة
if (event.origin !== 'https://your-domain.com') {
console.warn('رسالة من أصل غير موثوق:', event.origin);
return;
}
// التحقق من صحة بيانات الرسالة
if (!event.data || typeof event.data.action !== 'string') {
console.warn('تنسيق رسالة غير صالح');
return;
}
// معالجة الإجراءات المدرجة في القائمة البيضاء فقط
const allowedActions = ['skipWaiting', 'clearCache'];
if (!allowedActions.includes(event.data.action)) {
console.warn('إجراء غير مصرح به:', event.data.action);
return;
}
// معالجة الإجراء
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
استراتيجيات التخزين المؤقت الآمنة
تنفيذ التخزين المؤقت مع وضع الأمان في الاعتبار لحماية البيانات الحساسة.
// إدارة ذاكرة التخزين المؤقت الآمنة
const SENSITIVE_PATHS = [
'/api/user',
'/api/auth',
'/api/payment',
'/profile'
];
// التحقق مما إذا كان عنوان URL يحتوي على بيانات حساسة
function isSensitiveRequest(url) {
return SENSITIVE_PATHS.some(path => url.pathname.includes(path));
}
// التخزين المؤقت مع فحوصات الأمان
async function secureCacheResponse(request, response) {
const url = new URL(request.url);
// لا تخزن البيانات الحساسة مؤقتًا
if (isSensitiveRequest(url)) {
return response;
}
// لا تخزن مؤقتًا إذا كان الرد يحتوي على توجيه no-store
const cacheControl = response.headers.get('cache-control');
if (cacheControl && cacheControl.includes('no-store')) {
return response;
}
// لا تخزن الطلبات المصادق عليها مؤقتًا
if (request.headers.has('authorization')) {
return response;
}
// آمن للتخزين المؤقت
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}
// مسح البيانات الحساسة عند تسجيل الخروج
self.addEventListener('message', async (event) => {
if (event.data.action === 'logout') {
const cache = await caches.open(CACHE_NAME);
const requests = await cache.keys();
// إزالة البيانات الحساسة المخزنة مؤقتًا
for (const request of requests) {
const url = new URL(request.url);
if (isSensitiveRequest(url)) {
await cache.delete(request);
}
}
}
});
تخزين وإدارة الرموز
تخزين رموز المصادقة بشكل صحيح لمنع سرقة الرموز وهجمات XSS.
<script>
// أفضل ممارسات التخزين الآمن للرموز
class SecureStorage {
// تخزين رمز الوصول في الذاكرة (الأكثر أمانًا، يُفقد عند التحديث)
static accessToken = null;
// تخزين رمز التحديث في ملف تعريف ارتباط httpOnly (جانب الخادم فقط)
static setAccessToken(token) {
this.accessToken = token;
// لا تخزن في localStorage
}
static getAccessToken() {
return this.accessToken;
}
static clearAccessToken() {
this.accessToken = null;
}
// استخدم ملفات تعريف ارتباط httpOnly لرموز التحديث (تعيينها بواسطة الخادم)
static async refreshAccessToken() {
const response = await fetch('/api/refresh-token', {
method: 'POST',
credentials: 'include' // إرسال ملف تعريف ارتباط httpOnly
});
if (response.ok) {
const data = await response.json();
this.setAccessToken(data.accessToken);
return data.accessToken;
}
throw new Error('فشل تحديث الرمز');
}
}
// استدعاءات API آمنة مع الرمز
async function secureApiCall(url, options = {}) {
let token = SecureStorage.getAccessToken();
// حاول التحديث إذا كان الرمز مفقودًا
if (!token) {
token = await SecureStorage.refreshAccessToken();
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
// معالجة انتهاء صلاحية الرمز
if (response.status === 401) {
token = await SecureStorage.refreshAccessToken();
return secureApiCall(url, options); // إعادة المحاولة برمز جديد
}
return response;
}
// مسح الرموز عند تسجيل الخروج
function logout() {
SecureStorage.clearAccessToken();
// إخطار عامل الخدمة
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
action: 'logout'
});
}
// مسح البيانات الحساسة
sessionStorage.clear();
}
</script>
نصيحة: لا تقم أبدًا بتخزين الرموز الحساسة في localStorage أو sessionStorage. إنها عرضة لهجمات XSS. استخدم ملفات تعريف ارتباط httpOnly لرموز التحديث والتخزين في الذاكرة لرموز الوصول.
منع XSS
الحماية من هجمات البرمجة النصية عبر المواقع من خلال معالجة الإدخال المناسبة وترميز الإخراج.
<script>
// تطهير مدخلات المستخدم لمنع XSS
function sanitizeHTML(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
// استخدم DOMPurify لمحتوى HTML الغني
function sanitizeRichHTML(html) {
// قم بتضمين مكتبة DOMPurify: https://github.com/cure53/DOMPurify
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target']
});
}
// عرض محتوى المستخدم بأمان
function displayUserContent(content) {
const container = document.getElementById('userContent');
// سيء: عرضة لـ XSS
// container.innerHTML = content;
// جيد: آمن من XSS
container.textContent = content;
// أو استخدم HTML المطهر إذا كان التنسيق مطلوبًا
container.innerHTML = sanitizeRichHTML(content);
}
// التحقق من صحة وتطهير مدخلات النموذج
function validateInput(input, type) {
switch (type) {
case 'email':
return /^[\w.-]+@[\w.-]+\.\w+$/.test(input);
case 'url':
try {
new URL(input);
return input.startsWith('https://');
} catch {
return false;
}
case 'alphanumeric':
return /^[a-zA-Z0-9]+$/.test(input);
default:
return false;
}
}
// مثال على معالجة النموذج
document.getElementById('userForm').addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
// التحقق من صحة المدخلات
if (!validateInput(email, 'email')) {
alert('عنوان بريد إلكتروني غير صالح');
return;
}
// تطهير قبل الإرسال
const data = {
name: sanitizeHTML(name),
email: sanitizeHTML(email)
};
// الإرسال إلى الخادم
fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
});
</script>
تكوين CORS
تكوين مشاركة الموارد عبر المصدر بشكل صحيح للتحكم في الوصول إلى API.
// تكوين CORS من جانب الخادم (Node.js/Express)
const cors = require('cors');
// CORS مقيد (موصى به)
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://your-domain.com',
'https://app.your-domain.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('غير مسموح بواسطة CORS'));
}
},
credentials: true, // السماح بملفات تعريف الارتباط
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400 // تخزين preflight مؤقتًا لمدة 24 ساعة
};
app.use(cors(corsOptions));
// سياسات CORS مختلفة لمسارات مختلفة
app.use('/api/public', cors()); // API عامة
app.use('/api/private', cors(corsOptions)); // API مقيدة
تمرين:
- نفذ فرض HTTPS ورؤوس HSTS في PWA الخاص بك
- قم بتكوين سياسة أمان محتوى صارمة واختبر الانتهاكات
- قم بتأمين عامل الخدمة الخاص بك بفحوصات الأصل والتحقق من صحة الطلب
- نفذ التخزين الآمن للرموز باستخدام ملفات تعريف ارتباط httpOnly والتخزين في الذاكرة
- أضف إجراءات منع XSS بما في ذلك تطهير المدخلات وترميز الإخراج
قائمة التحقق من الأمان
- HTTPS في كل مكان: فرض HTTPS مع HSTS وupgrade-insecure-requests
- سياسة أمان المحتوى: تنفيذ CSP صارم بدون 'unsafe-inline' أو 'unsafe-eval'
- نطاق عامل الخدمة: قصر نطاق عامل الخدمة لتقليل سطح الهجوم
- أمان الرمز: استخدم ملفات تعريف ارتباط httpOnly لرموز التحديث، والذاكرة لرموز الوصول
- التحقق من صحة الإدخال: التحقق من صحة وتطهير جميع مدخلات المستخدم
- ترميز الإخراج: ترميز جميع محتوى المستخدم قبل العرض
- تكوين CORS: قائمة بيضاء لمصادر محددة، تجنب استخدام حرف بدل (*)
- التخزين المؤقت الآمن: لا تخزن البيانات الحساسة أبدًا (رموز المصادقة، معلومات الدفع، البيانات الشخصية)
- التحديثات المنتظمة: حافظ على تحديث التبعيات لإصلاح ثغرات الأمان
- رؤوس الأمان: تنفيذ X-Frame-Options وX-Content-Type-Options وReferrer-Policy