Update v1.2.0-beta - Dynamic context menu & permissions
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
✨ New Features: - Dynamic permission-based context menus for files and folders - Support for collaborative folder access control - Upload to specific folders including shared folders - Changelog modal for version updates - Improved dark mode synchronization 🐛 Bug Fixes: - Fixed context menu displaying incorrect options - Fixed CSS !important override preventing dynamic menu behavior - Fixed folder collaboration permission checks - Fixed breadcrumb navigation with empty segments - Fixed "Premature close" error loop in attachments - Fixed missing user variable in admin routes - Fixed avatar loading COEP policy issues 🔒 Security: - Added security middleware (CSRF, rate limiting, input validation) - Fixed collaboration folder access validation - Improved shared folder permission handling 🎨 UI/UX Improvements: - Removed Actions column from folder view - Context menu now properly hides/shows based on permissions - Better visual feedback for collaborative folders - Improved upload flow with inline modals 🧹 Code Quality: - Added collaboration data to folder routes - Refactored context menu logic for better maintainability - Added debug logging for troubleshooting - Improved file upload handling with chunking support
This commit is contained in:
170
Middlewares/csrfProtectionMiddleware.js
Normal file
170
Middlewares/csrfProtectionMiddleware.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Middleware de protection CSRF (Cross-Site Request Forgery)
|
||||
* Génère et valide des tokens CSRF pour les requêtes sensibles
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { logger } = require('../config/logs');
|
||||
|
||||
// Stockage en mémoire des tokens CSRF (en production, utiliser Redis ou une base de données)
|
||||
const csrfTokens = new Map();
|
||||
|
||||
// Durée de vie d'un token CSRF (30 minutes)
|
||||
const TOKEN_LIFETIME = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Génère un token CSRF pour une session
|
||||
*/
|
||||
const generateCsrfToken = (sessionId) => {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = Date.now() + TOKEN_LIFETIME;
|
||||
|
||||
csrfTokens.set(sessionId, {
|
||||
token,
|
||||
expiresAt
|
||||
});
|
||||
|
||||
// Nettoyer les tokens expirés toutes les 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [id, data] of csrfTokens.entries()) {
|
||||
if (data.expiresAt < now) {
|
||||
csrfTokens.delete(id);
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Valide un token CSRF
|
||||
*/
|
||||
const validateCsrfToken = (sessionId, token) => {
|
||||
const storedData = csrfTokens.get(sessionId);
|
||||
|
||||
if (!storedData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storedData.expiresAt < Date.now()) {
|
||||
csrfTokens.delete(sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
return storedData.token === token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware pour ajouter le token CSRF à la requête
|
||||
*/
|
||||
const csrfTokenMiddleware = (req, res, next) => {
|
||||
// Générer un ID de session si nécessaire
|
||||
if (!req.session) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const sessionId = req.session.id || req.sessionID;
|
||||
|
||||
// Générer ou récupérer le token CSRF
|
||||
let csrfToken = null;
|
||||
const storedData = csrfTokens.get(sessionId);
|
||||
|
||||
if (storedData && storedData.expiresAt > Date.now()) {
|
||||
csrfToken = storedData.token;
|
||||
} else {
|
||||
csrfToken = generateCsrfToken(sessionId);
|
||||
}
|
||||
|
||||
// Ajouter le token aux locals pour l'utiliser dans les vues
|
||||
res.locals.csrfToken = csrfToken;
|
||||
|
||||
// Ajouter une méthode helper
|
||||
req.csrfToken = () => csrfToken;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware de validation CSRF pour les méthodes POST, PUT, DELETE
|
||||
*/
|
||||
const csrfProtectionMiddleware = (req, res, next) => {
|
||||
// Ignorer les requêtes GET, HEAD, OPTIONS
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Ignorer certaines routes (API publiques, webhooks, etc.)
|
||||
const exemptPaths = [
|
||||
'/auth/activedirectory/callback',
|
||||
'/auth/discord/callback',
|
||||
'/api/webhook'
|
||||
];
|
||||
|
||||
if (exemptPaths.some(path => req.path.startsWith(path))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Vérifier si la session existe
|
||||
if (!req.session) {
|
||||
logger.warn(`CSRF protection: No session found for ${req.method} ${req.path} from ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
error: 'Session invalide',
|
||||
code: 'NO_SESSION'
|
||||
});
|
||||
}
|
||||
|
||||
const sessionId = req.session.id || req.sessionID;
|
||||
|
||||
// Récupérer le token CSRF de la requête
|
||||
const token = req.body._csrf ||
|
||||
req.query._csrf ||
|
||||
req.headers['x-csrf-token'] ||
|
||||
req.headers['csrf-token'];
|
||||
|
||||
if (!token) {
|
||||
logger.warn(`CSRF protection: No token provided for ${req.method} ${req.path} from ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
error: 'Token CSRF manquant',
|
||||
code: 'CSRF_TOKEN_MISSING'
|
||||
});
|
||||
}
|
||||
|
||||
// Valider le token
|
||||
if (!validateCsrfToken(sessionId, token)) {
|
||||
logger.warn(`CSRF protection: Invalid token for ${req.method} ${req.path} from ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
error: 'Token CSRF invalide ou expiré',
|
||||
code: 'CSRF_TOKEN_INVALID'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Route pour obtenir un nouveau token CSRF
|
||||
*/
|
||||
const getCsrfToken = (req, res) => {
|
||||
const sessionId = req.session?.id || req.sessionID;
|
||||
|
||||
if (!sessionId) {
|
||||
return res.status(400).json({
|
||||
error: 'Session invalide'
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateCsrfToken(sessionId);
|
||||
|
||||
res.json({
|
||||
csrfToken: token
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
csrfTokenMiddleware,
|
||||
csrfProtectionMiddleware,
|
||||
getCsrfToken,
|
||||
generateCsrfToken,
|
||||
validateCsrfToken
|
||||
};
|
||||
202
Middlewares/inputValidationMiddleware.js
Normal file
202
Middlewares/inputValidationMiddleware.js
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Middleware de validation et d'assainissement des entrées utilisateur
|
||||
* Protège contre les injections SQL, XSS, et autres attaques
|
||||
*/
|
||||
|
||||
const { logger } = require('../config/logs');
|
||||
|
||||
/**
|
||||
* Nettoie une chaîne de caractères pour prévenir les injections
|
||||
*/
|
||||
const sanitizeString = (str) => {
|
||||
if (typeof str !== 'string') return str;
|
||||
|
||||
// Supprime les caractères null bytes
|
||||
str = str.replace(/\0/g, '');
|
||||
|
||||
// Encode les caractères spéciaux HTML
|
||||
str = str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
|
||||
return str.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Valide un nom de fichier/dossier
|
||||
*/
|
||||
const validateFileName = (filename) => {
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
return { valid: false, error: 'Nom de fichier invalide' };
|
||||
}
|
||||
|
||||
// Limite de longueur
|
||||
if (filename.length > 255) {
|
||||
return { valid: false, error: 'Nom de fichier trop long (max 255 caractères)' };
|
||||
}
|
||||
|
||||
// Caractères interdits dans les noms de fichiers Windows/Linux
|
||||
const forbiddenChars = /[<>:"\/\\|?*\x00-\x1f]/g;
|
||||
if (forbiddenChars.test(filename)) {
|
||||
return { valid: false, error: 'Caractères interdits dans le nom de fichier' };
|
||||
}
|
||||
|
||||
// Noms réservés Windows
|
||||
const reservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
if (reservedNames.test(filename.split('.')[0])) {
|
||||
return { valid: false, error: 'Nom de fichier réservé' };
|
||||
}
|
||||
|
||||
// Vérifier les tentatives de traversée de répertoire
|
||||
if (filename.includes('..') || filename.includes('./') || filename.includes('.\\')) {
|
||||
return { valid: false, error: 'Tentative de traversée de répertoire détectée' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Valide un chemin de dossier
|
||||
*/
|
||||
const validatePath = (path) => {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return { valid: false, error: 'Chemin invalide' };
|
||||
}
|
||||
|
||||
// Vérifier les tentatives de traversée de répertoire
|
||||
if (path.includes('..') || path.includes('~')) {
|
||||
return { valid: false, error: 'Tentative de traversée de répertoire détectée' };
|
||||
}
|
||||
|
||||
// Vérifier les chemins absolus non autorisés
|
||||
if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) {
|
||||
return { valid: false, error: 'Chemin absolu non autorisé' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Valide un ID utilisateur
|
||||
*/
|
||||
const validateUserId = (userId) => {
|
||||
if (!userId) {
|
||||
return { valid: false, error: 'ID utilisateur manquant' };
|
||||
}
|
||||
|
||||
// Accepte les formats Discord (snowflake) ou alphanumériques
|
||||
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(userId)) {
|
||||
return { valid: false, error: 'Format d\'ID utilisateur invalide' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Valide une adresse email
|
||||
*/
|
||||
const validateEmail = (email) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return { valid: false, error: 'Email invalide' };
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return { valid: false, error: 'Format d\'email invalide' };
|
||||
}
|
||||
|
||||
if (email.length > 254) {
|
||||
return { valid: false, error: 'Email trop long' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Nettoie récursivement un objet
|
||||
*/
|
||||
const sanitizeObject = (obj) => {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return sanitizeString(obj);
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => sanitizeObject(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const sanitized = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
sanitized[key] = sanitizeObject(value);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware principal de validation
|
||||
*/
|
||||
const inputValidationMiddleware = (req, res, next) => {
|
||||
try {
|
||||
// Log des requêtes suspectes
|
||||
const suspiciousPatterns = [
|
||||
/<script/i,
|
||||
/javascript:/i,
|
||||
/on\w+\s*=/i,
|
||||
/\.\.\//,
|
||||
/union\s+select/i,
|
||||
/insert\s+into/i,
|
||||
/delete\s+from/i,
|
||||
/drop\s+table/i,
|
||||
/exec\s*\(/i,
|
||||
/eval\s*\(/i
|
||||
];
|
||||
|
||||
const checkSuspicious = (value) => {
|
||||
if (typeof value === 'string') {
|
||||
return suspiciousPatterns.some(pattern => pattern.test(value));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Vérifier le body
|
||||
if (req.body && typeof req.body === 'object') {
|
||||
const bodyStr = JSON.stringify(req.body);
|
||||
if (suspiciousPatterns.some(pattern => pattern.test(bodyStr))) {
|
||||
logger.warn(`Suspicious input detected in request body from ${req.ip}: ${req.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les query params
|
||||
if (req.query && typeof req.query === 'object') {
|
||||
const queryStr = JSON.stringify(req.query);
|
||||
if (suspiciousPatterns.some(pattern => pattern.test(queryStr))) {
|
||||
logger.warn(`Suspicious input detected in query params from ${req.ip}: ${req.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Continuer sans bloquer (logging only pour ne pas casser l'app)
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Error in input validation middleware:', error);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputValidationMiddleware,
|
||||
sanitizeString,
|
||||
sanitizeObject,
|
||||
validateFileName,
|
||||
validatePath,
|
||||
validateUserId,
|
||||
validateEmail
|
||||
};
|
||||
194
Middlewares/rateLimitMiddleware.js
Normal file
194
Middlewares/rateLimitMiddleware.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Middleware de rate limiting renforcé
|
||||
* Protège contre les abus et les attaques par force brute
|
||||
*/
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { logger } = require('../config/logs');
|
||||
|
||||
/**
|
||||
* Rate limiter général pour toutes les requêtes
|
||||
*/
|
||||
const generalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000, // Limite de 1000 requêtes par fenêtre
|
||||
message: {
|
||||
error: 'Trop de requêtes depuis cette adresse IP, veuillez réessayer plus tard.',
|
||||
retryAfter: '15 minutes'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
// Skip pour localhost
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Rate limit exceeded for ${req.ip} on ${req.path}`);
|
||||
res.status(429).json({
|
||||
error: 'Trop de requêtes',
|
||||
message: 'Vous avez dépassé la limite de requêtes autorisées. Veuillez réessayer dans 15 minutes.',
|
||||
retryAfter: 900
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter strict pour les endpoints sensibles (authentification)
|
||||
*/
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // Seulement 5 tentatives de connexion
|
||||
skipSuccessfulRequests: true, // Ne pas compter les tentatives réussies
|
||||
message: {
|
||||
error: 'Trop de tentatives de connexion, votre compte est temporairement verrouillé.',
|
||||
retryAfter: '15 minutes'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Auth rate limit exceeded for ${req.ip} on ${req.path}`);
|
||||
res.status(429).json({
|
||||
error: 'Compte temporairement verrouillé',
|
||||
message: 'Trop de tentatives de connexion. Veuillez réessayer dans 15 minutes.',
|
||||
retryAfter: 900
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pour les uploads
|
||||
*/
|
||||
const uploadLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 heure
|
||||
max: 100, // 100 uploads par heure
|
||||
message: {
|
||||
error: 'Limite d\'upload atteinte',
|
||||
retryAfter: '1 heure'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Upload rate limit exceeded for ${req.ip}`);
|
||||
res.status(429).json({
|
||||
error: 'Limite d\'upload dépassée',
|
||||
message: 'Vous avez atteint la limite d\'uploads autorisés. Veuillez réessayer dans 1 heure.',
|
||||
retryAfter: 3600
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pour les API
|
||||
*/
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 60, // 60 requêtes par minute
|
||||
message: {
|
||||
error: 'Limite d\'API atteinte',
|
||||
retryAfter: '1 minute'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`API rate limit exceeded for ${req.ip} on ${req.path}`);
|
||||
res.status(429).json({
|
||||
error: 'Limite d\'API dépassée',
|
||||
message: 'Vous avez dépassé la limite de requêtes API autorisées. Veuillez réessayer dans 1 minute.',
|
||||
retryAfter: 60
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pour la création de dossiers/fichiers
|
||||
*/
|
||||
const createLimiter = rateLimit({
|
||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||
max: 50, // 50 créations par 10 minutes
|
||||
message: {
|
||||
error: 'Limite de création atteinte',
|
||||
retryAfter: '10 minutes'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Create rate limit exceeded for ${req.ip} on ${req.path}`);
|
||||
res.status(429).json({
|
||||
error: 'Limite de création dépassée',
|
||||
message: 'Vous avez dépassé la limite de créations autorisées. Veuillez réessayer dans 10 minutes.',
|
||||
retryAfter: 600
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pour les suppressions
|
||||
*/
|
||||
const deleteLimiter = rateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 30, // 30 suppressions par 5 minutes
|
||||
message: {
|
||||
error: 'Limite de suppression atteinte',
|
||||
retryAfter: '5 minutes'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Delete rate limit exceeded for ${req.ip} on ${req.path}`);
|
||||
res.status(429).json({
|
||||
error: 'Limite de suppression dépassée',
|
||||
message: 'Vous avez dépassé la limite de suppressions autorisées. Veuillez réessayer dans 5 minutes.',
|
||||
retryAfter: 300
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter strict pour la recherche d'utilisateurs (prévient l'énumération)
|
||||
*/
|
||||
const userSearchLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
max: 10, // 10 recherches par minute
|
||||
message: {
|
||||
error: 'Limite de recherche atteinte',
|
||||
retryAfter: '1 minute'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => {
|
||||
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
logger.warn(`User search rate limit exceeded for ${req.ip}`);
|
||||
res.status(429).json({
|
||||
error: 'Limite de recherche dépassée',
|
||||
message: 'Vous avez dépassé la limite de recherches autorisées. Veuillez réessayer dans 1 minute.',
|
||||
retryAfter: 60
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
generalLimiter,
|
||||
authLimiter,
|
||||
uploadLimiter,
|
||||
apiLimiter,
|
||||
createLimiter,
|
||||
deleteLimiter,
|
||||
userSearchLimiter
|
||||
};
|
||||
76
Middlewares/securityHeadersMiddleware.js
Normal file
76
Middlewares/securityHeadersMiddleware.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Middleware de sécurité pour ajouter les headers HTTP sécurisés
|
||||
* Conforme aux bonnes pratiques OWASP
|
||||
*/
|
||||
|
||||
const securityHeadersMiddleware = (req, res, next) => {
|
||||
// Désactive l'envoi de l'en-tête X-Powered-By pour ne pas révéler la stack technique
|
||||
res.removeHeader('X-Powered-By');
|
||||
|
||||
// Content Security Policy (CSP) - Protège contre les attaques XSS
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
[
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://code.jquery.com https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
|
||||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com",
|
||||
"img-src 'self' data: https: blob:",
|
||||
"font-src 'self' https://cdnjs.cloudflare.com https://fonts.gstatic.com",
|
||||
"connect-src 'self' ws: wss: https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'"
|
||||
].join('; ')
|
||||
);
|
||||
|
||||
// X-Content-Type-Options - Empêche le navigateur de deviner le type MIME
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// X-Frame-Options - Protège contre le clickjacking
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
|
||||
// X-XSS-Protection - Active la protection XSS du navigateur
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// Strict-Transport-Security (HSTS) - Force HTTPS
|
||||
if (req.secure || process.env.NODE_ENV === 'production') {
|
||||
res.setHeader(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
}
|
||||
|
||||
// Referrer-Policy - Contrôle les informations de référence envoyées
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Permissions-Policy - Contrôle les fonctionnalités du navigateur
|
||||
res.setHeader(
|
||||
'Permissions-Policy',
|
||||
[
|
||||
'camera=()',
|
||||
'microphone=()',
|
||||
'geolocation=()',
|
||||
'payment=()',
|
||||
'usb=()',
|
||||
'magnetometer=()',
|
||||
'accelerometer=()',
|
||||
'gyroscope=()'
|
||||
].join(', ')
|
||||
);
|
||||
|
||||
// X-Permitted-Cross-Domain-Policies - Restreint les politiques cross-domain
|
||||
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
||||
|
||||
// Cross-Origin-Embedder-Policy - Désactivé pour permettre les ressources externes (avatars, images CDN)
|
||||
// res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
|
||||
// Cross-Origin-Opener-Policy - Isole le contexte de navigation
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
|
||||
// Cross-Origin-Resource-Policy - Contrôle le partage de ressources
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = securityHeadersMiddleware;
|
||||
Reference in New Issue
Block a user