Introduction “Notre app est-elle sécurisée ?” Cette question revient souvent, suivie d’une longue liste d’inquiétudes parfois disproportionnées. Entre l’insouciance dangereuse et la paranoia paralysante, il existe un chemin pragmatique pour sécuriser efficacement une application web.
La sécurité n’est pas binaire. Il s’agit de gérer des risques selon leur probabilité et leur impact, en appliquant des mesures proportionnées aux menaces réelles.
Évaluation des risques : par où commencer Le TOP 10 OWASP dans la vraie vie Priorisation par impact réel // Matrice risque/effort pour prioriser les actions sécurité const securityRiskMatrix = { CRITIQUE: { impact: 'Très élevé', likelihood: 'Élevée', examples: [ 'Injection SQL sur endpoint public', 'Authentication bypass', 'Exposition de données sensibles' ], action: 'Fix immédiat, arrêt déploiements si nécessaire' }, ÉLEVÉ: { impact: 'Élevé', likelihood: 'Moyenne', examples: [ 'XSS sur formulaires utilisateur', 'CSRF sur actions critiques', 'Permissions trop larges' ], action: 'Fix dans la semaine, tests prioritaires' }, MODÉRÉ: { impact: 'Moyen', likelihood: 'Faible', examples: [ 'Information disclosure mineure', 'Rate limiting insuffisant', 'Headers sécurité manquants' ], action: 'Fix dans le sprint suivant' }, FAIBLE: { impact: 'Faible', likelihood: 'Très faible', examples: [ 'Versions de libs légèrement obsolètes', 'Logs trop verbeux en dev', 'Crypto legacy mais fonctionnel' ], action: 'Amélioration continue, pas urgent' } }; Audit rapide avec checklist # 🔒 Security Checklist - Audit Express ## Authentication & Authorization - [ ] Mots de passe hashés (bcrypt, scrypt, ou Argon2) - [ ] Sessions sécurisées (httpOnly, secure, sameSite) - [ ] Tokens JWT signés et avec expiration - [ ] 2FA disponible pour admins/comptes sensibles - [ ] Rate limiting sur login/register ## Input Validation - [ ] Validation côté serveur systématique - [ ] Paramètres SQL préparés (pas de concaténation) - [ ] Échappement HTML pour affichage user content - [ ] File upload restrictions (type, taille, scan virus) - [ ] Validation des redirects (pas d'open redirect) ## Data Protection - [ ] HTTPS partout (HSTS activé) - [ ] Chiffrement des données sensibles en base - [ ] Backup chiffrés - [ ] Logs ne contiennent pas de secrets - [ ] Variables d'environnement pour secrets ## Infrastructure - [ ] Firewall correctement configuré - [ ] Services internes non exposés publiquement - [ ] Updates sécurité automatiques - [ ] Monitoring tentatives d'intrusion - [ ] Plan de réponse incident documenté Contexte métier : adapter les mesures // Sécurité proportionnée selon le contexte const securityByContext = { blogPersonnel: { risques: ['Spam', 'Défacement', 'SEO spam'], mesures: [ 'Rate limiting basique', 'Validation inputs formulaires', 'HTTPS avec Let\'s Encrypt', 'Backup automatiques' ], effort: 'Minimal, focus sur disponibilité' }, eCommerce: { risques: ['Vol données client', 'Fraude paiement', 'Manipulation prix'], mesures: [ 'PCI DSS compliance', 'Chiffrement bout en bout', 'Monitoring transactions suspectes', 'Audit logs détaillés' ], effort: 'Élevé, investissement business critique' }, startupB2B: { risques: ['Accès données clients', 'IP theft', 'Service disruption'], mesures: [ 'RBAC granulaire', 'Encryption at rest', 'SOC2 préparation', 'Incident response plan' ], effort: 'Progressif selon croissance' }, fintech: { risques: ['Régulation', 'Fraude massive', 'Réputation'], mesures: [ 'Multiple compliance frameworks', 'Zero-trust architecture', 'Continuous monitoring', 'Pentesting régulier' ], effort: 'Maximum, coût de non-conformité énorme' } }; Authentification robuste Stratégie de mots de passe moderne Hashing sécurisé // Configuration bcrypt/scrypt pour 2025 const passwordHashing = { bcrypt: { rounds: 12, // 2^12 iterations (adaptable selon hardware) library: 'bcryptjs', // Pure JS pour compatibilité example: async (password) => { const saltRounds = 12; return await bcrypt.hash(password, saltRounds); } }, scrypt: { recommended: true, config: { N: 32768, // CPU/memory cost r: 8, // Block size p: 1, // Parallelization keylen: 64 // Output length }, example: (password, salt) => { return new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, { N: 32768, r: 8, p: 1 }, (err, derivedKey) => { if (err) reject(err); resolve(derivedKey); }); }); } } }; // Implémentation pratique class PasswordService { async hashPassword(plainPassword) { try { // Générer salt aléatoire const salt = crypto.randomBytes(32); // Hash avec scrypt const hashedPassword = await new Promise((resolve, reject) => { crypto.scrypt(plainPassword, salt, 64, { N: 32768, r: 8, p: 1 }, (err, derivedKey) => { if (err) reject(err); resolve(derivedKey); }); }); // Combiner salt + hash pour stockage return { hash: hashedPassword.toString('hex'), salt: salt.toString('hex') }; } catch (error) { throw new Error('Password hashing failed'); } } async verifyPassword(plainPassword, storedHash, storedSalt) { try { const salt = Buffer.from(storedSalt, 'hex'); const hashedPassword = await new Promise((resolve, reject) => { crypto.scrypt(plainPassword, salt, 64, { N: 32768, r: 8, p: 1 }, (err, derivedKey) => { if (err) reject(err); resolve(derivedKey); }); }); return crypto.timingSafeEqual( Buffer.from(storedHash, 'hex'), hashedPassword ); } catch (error) { return false; // En cas d'erreur, refuse l'authentification } } } Politique de mots de passe équilibrée // Validation moderne (NIST guidelines 2025) const passwordPolicy = { // Ce qu'on FAIT requirements: { minLength: 8, maxLength: 128, // Pas de limite basse arbitraire checkBreachedPasswords: true, // API HaveIBeenPwned allowPassphrases: true, allowPasswordManagers: true }, // Ce qu'on ÉVITE (anciennes pratiques) deprecated: { complexityRules: false, // Pas d'obligation spéciaux/majuscules regularExpiration: false, // Pas de changement forcé périodique preventReuse: false, // Sauf cas très spécifiques security_questions: false // Remplacés par 2FA }, implementation: { clientSideCheck: 'Feedback temps réel pour UX', serverSideValidation: 'Validation finale obligatoire', breachCheck: 'Async après validation basique', userEducation: 'Guide pour password manager' } }; // Validation côté serveur class PasswordValidator { constructor() { this.breachedPasswordsAPI = 'https://api.pwnedpasswords.com/range/'; } async validate(password, userInfo = {}) { const errors = []; // Longueur if (password.length < 8) { errors.push('Password must be at least 8 characters'); } if (password.length > 128) { errors.push('Password too long (max 128 characters)'); } // Pas d'infos personnelles évidentes if (userInfo.email) { const emailPart = userInfo.email.split('@')[0].toLowerCase(); if (password.toLowerCase().includes(emailPart)) { errors.push('Password should not contain parts of your email'); } } // Check contre base de mots de passe compromis try { const isBreached = await this.checkBreachedPassword(password); if (isBreached) { errors.push('This password has been compromised in data breaches'); } } catch (error) { // En cas d'erreur API, on continue (ne pas bloquer l'utilisateur) console.warn('Breach check failed:', error.message); } return { valid: errors.length === 0, errors, strength: this.calculateStrength(password) }; } async checkBreachedPassword(password) { // Hash SHA-1 du mot de passe const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase(); const prefix = hash.substring(0, 5); const suffix = hash.substring(5); // API k-Anonymity de HaveIBeenPwned const response = await fetch(`${this.breachedPasswordsAPI}${prefix}`); const data = await response.text(); // Chercher le suffixe dans les résultats return data.split('\n').some(line => line.startsWith(suffix)); } } Sessions et tokens sécurisés Configuration session cookies // Configuration Express sessions sécurisées const sessionConfig = { secret: process.env.SESSION_SECRET, // 256 bits minimum name: 'sessionId', // Pas 'connect.sid' par défaut cookie: { secure: process.env.NODE_ENV === 'production', // HTTPS only httpOnly: true, // Pas accessible en JavaScript maxAge: 30 * 60 * 1000, // 30 minutes sameSite: 'strict' // Protection CSRF basique }, resave: false, saveUninitialized: false, store: new RedisStore({ client: redisClient, prefix: 'sess:', ttl: 1800 // 30 minutes en secondes }) }; // Middleware de renouvellement session const renewSession = (req, res, next) => { if (req.session && req.session.user) { // Renouveler la session si proche expiration const timeLeft = req.session.cookie.maxAge; if (timeLeft < 5 * 60 * 1000) { // 5 minutes restantes req.session.cookie.maxAge = 30 * 60 * 1000; // Reset à 30 min } } next(); }; JWT stratégie défensive // Implémentation JWT sécurisée class JWTService { constructor() { this.accessTokenSecret = process.env.JWT_ACCESS_SECRET; this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET; this.issuer = 'myapp.com'; this.audience = 'myapp-users'; } generateTokenPair(userId, roles = []) { const now = Math.floor(Date.now() / 1000); const accessToken = jwt.sign( { sub: userId, roles, type: 'access', iat: now, exp: now + (15 * 60), // 15 minutes iss: this.issuer, aud: this.audience }, this.accessTokenSecret, { algorithm: 'HS256' } ); const refreshToken = jwt.sign( { sub: userId, type: 'refresh', iat: now, exp: now + (7 * 24 * 60 * 60), // 7 jours iss: this.issuer, aud: this.audience }, this.refreshTokenSecret, { algorithm: 'HS256' } ); return { accessToken, refreshToken }; } async verifyAccessToken(token) { try { const payload = jwt.verify(token, this.accessTokenSecret, { algorithms: ['HS256'], issuer: this.issuer, audience: this.audience }); if (payload.type !== 'access') { throw new Error('Invalid token type'); } // Vérifier que l'utilisateur existe encore const user = await this.getUserById(payload.sub); if (!user || !user.active) { throw new Error('User no longer active'); } return payload; } catch (error) { throw new Error('Invalid access token'); } } async refreshTokens(refreshToken) { try { const payload = jwt.verify(refreshToken, this.refreshTokenSecret); if (payload.type !== 'refresh') { throw new Error('Invalid token type'); } // Vérifier en base que le refresh token est valide const storedToken = await this.getStoredRefreshToken(payload.sub, refreshToken); if (!storedToken) { throw new Error('Refresh token revoked or invalid'); } // Révoquer l'ancien refresh token await this.revokeRefreshToken(refreshToken); // Générer nouveaux tokens const user = await this.getUserById(payload.sub); const newTokens = this.generateTokenPair(user.id, user.roles); // Stocker le nouveau refresh token await this.storeRefreshToken(user.id, newTokens.refreshToken); return newTokens; } catch (error) { throw new Error('Invalid refresh token'); } } } Protection des données sensibles Chiffrement adapté aux besoins Données au repos // Stratégie chiffrement par type de données const encryptionStrategy = { // Très sensible : PII, secrets, tokens highSensitive: { algorithm: 'aes-256-gcm', keyDerivation: 'PBKDF2', implementation: class HighSensitiveEncryption { constructor() { this.masterKey = process.env.MASTER_ENCRYPTION_KEY; } encrypt(plaintext, associatedData = '') { const salt = crypto.randomBytes(16); const iv = crypto.randomBytes(12); // Dériver clé de chiffrement const key = crypto.pbkdf2Sync(this.masterKey, salt, 100000, 32, 'sha256'); const cipher = crypto.createCipher('aes-256-gcm', key); cipher.setAAD(Buffer.from(associatedData)); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return { encrypted, salt: salt.toString('hex'), iv: iv.toString('hex'), authTag: authTag.toString('hex') }; } } }, // Modéré : données métier, logs moderate: { algorithm: 'aes-256-cbc', note: 'Chiffrement simple mais efficace pour données moins critiques' }, // Transit : toujours HTTPS + HSTS inTransit: { tls: 'TLS 1.3 minimum', ciphers: 'Suites chiffrement modernes uniquement', certificates: 'Let\'s Encrypt ou CA reconnu' } }; // Utilitaire pour champs sensibles en DB class SensitiveField { constructor(encryptionKey) { this.encryptionKey = encryptionKey; } // Pour Sequelize/TypeORM get(value) { if (!value) return value; return this.decrypt(value); } set(value) { if (!value) return value; return this.encrypt(value); } encrypt(plaintext) { const cipher = crypto.createCipher('aes-256-cbc', this.encryptionKey); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; } decrypt(ciphertext) { const decipher = crypto.createDecipher('aes-256-cbc', this.encryptionKey); let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } } Gestion des secrets Variables d’environnement sécurisées # .env.example - Template pour équipe # Copier vers .env et remplir avec vraies valeurs # Base de données DATABASE_URL=postgresql://username:password@localhost:5432/dbname DATABASE_SSL=true # Authentification JWT_ACCESS_SECRET=generate-256-bit-random-string JWT_REFRESH_SECRET=generate-different-256-bit-string SESSION_SECRET=another-256-bit-random-string # Chiffrement MASTER_ENCRYPTION_KEY=yet-another-256-bit-key # APIs externes STRIPE_SECRET_KEY=sk_test_... SENDGRID_API_KEY=SG... AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... # Environnement NODE_ENV=development LOG_LEVEL=debug // Validation des secrets au démarrage class ConfigValidator { static validate() { const required = [ 'DATABASE_URL', 'JWT_ACCESS_SECRET', 'JWT_REFRESH_SECRET', 'SESSION_SECRET' ]; const missing = required.filter(key => !process.env[key]); if (missing.length > 0) { console.error('Missing required environment variables:', missing); process.exit(1); } // Vérifier la longueur des secrets const secrets = ['JWT_ACCESS_SECRET', 'JWT_REFRESH_SECRET', 'SESSION_SECRET']; secrets.forEach(secret => { const value = process.env[secret]; if (Buffer.from(value).length < 32) { // 256 bits minimum console.error(`${secret} must be at least 32 bytes (256 bits)`); process.exit(1); } }); console.log('✅ Configuration validation passed'); } } // Au démarrage de l'app ConfigValidator.validate(); Protection contre les attaques communes Injection et validation d’entrée Protection SQL Injection // ✅ Bonnes pratiques requêtes sécurisées class UserRepository { constructor(db) { this.db = db; } // Prepared statements (parameterized queries) async findByEmail(email) { const query = 'SELECT * FROM users WHERE email = $1 AND active = true'; const result = await this.db.query(query, [email]); return result.rows[0]; } // ORM avec validations async updateUser(userId, data) { // Validation avant requête const validatedData = this.validateUserData(data); // Query builder sécurisé (Knex exemple) return await this.db('users') .where('id', userId) .update(validatedData); } validateUserData(data) { const allowedFields = ['name', 'email', 'bio']; const sanitized = {}; allowedFields.forEach(field => { if (data[field] !== undefined) { sanitized[field] = this.sanitizeString(data[field]); } }); return sanitized; } sanitizeString(str) { if (typeof str !== 'string') return str; // Supprimer caractères de contrôle return str.replace(/[\x00-\x1F\x7F]/g, '') .trim() .substring(0, 1000); // Limite taille } } Protection XSS // Middleware anti-XSS global const xssProtection = { // Content Security Policy csp: { directives: { defaultSrc: ["'self'"], scriptSrc: [ "'self'", "'unsafe-inline'", // Éviter si possible "https://cdn.jsdelivr.net", "https://unpkg.com" ], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], imgSrc: ["'self'", "data:", "https:"], fontSrc: ["'self'", "https://fonts.gstatic.com"], connectSrc: ["'self'", "https://api.example.com"] }, reportOnly: false // true pour mode test }, // Headers sécurité securityHeaders: (req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); next(); } }; // Sanitisation côté serveur const DOMPurify = require('isomorphic-dompurify'); class ContentSanitizer { static sanitizeHTML(dirty) { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'], ALLOWED_ATTR: ['href'], ALLOW_DATA_ATTR: false }); } static sanitizeText(text) { return text.replace(/[<>'"&]/g, (match) => { const escapes = { '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' }; return escapes[match]; }); } } Rate limiting intelligent // Rate limiting adaptatif selon contexte class SmartRateLimiter { constructor(redis) { this.redis = redis; this.rules = { // Endpoints critiques login: { window: 900, max: 5, punishment: 3600 }, // 15min window, 5 attempts, 1h ban register: { window: 3600, max: 3, punishment: 7200 }, resetPassword: { window: 3600, max: 2, punishment: 3600 }, // APIs apiPublic: { window: 60, max: 100, punishment: 300 }, apiAuth: { window: 60, max: 1000, punishment: 120 }, // Upload fileUpload: { window: 3600, max: 10, punishment: 3600 } }; } async checkLimit(identifier, ruleKey, req = {}) { const rule = this.rules[ruleKey]; if (!rule) return { allowed: true }; const key = `rate:${ruleKey}:${identifier}`; const punishKey = `punish:${ruleKey}:${identifier}`; // Vérifier si en punition const isPunished = await this.redis.get(punishKey); if (isPunished) { return { allowed: false, reason: 'temporarily_banned', retryAfter: await this.redis.ttl(punishKey) }; } // Compter les tentatives const current = await this.redis.incr(key); if (current === 1) { await this.redis.expire(key, rule.window); } if (current > rule.max) { // Déclencher punition await this.redis.setex(punishKey, rule.punishment, 'banned'); // Log pour monitoring console.warn('Rate limit exceeded', { identifier, rule: ruleKey, attempts: current, ip: req.ip, userAgent: req.headers['user-agent'] }); return { allowed: false, reason: 'rate_limit_exceeded', retryAfter: rule.punishment }; } return { allowed: true, remaining: rule.max - current, resetTime: await this.redis.ttl(key) }; } // Middleware Express createMiddleware(ruleKey, getIdentifier = (req) => req.ip) { return async (req, res, next) => { const identifier = getIdentifier(req); const result = await this.checkLimit(identifier, ruleKey, req); if (!result.allowed) { return res.status(429).json({ error: 'Too Many Requests', code: result.reason, retryAfter: result.retryAfter }); } // Headers informatifs res.set({ 'X-RateLimit-Remaining': result.remaining, 'X-RateLimit-Reset': result.resetTime }); next(); }; } } // Usage const rateLimiter = new SmartRateLimiter(redisClient); app.post('/auth/login', rateLimiter.createMiddleware('login', req => req.ip), loginController ); app.post('/api/data', authenticate, // Après auth pour avoir userId rateLimiter.createMiddleware('apiAuth', req => req.userId), apiController ); Monitoring et réponse aux incidents Détection d’activité suspecte // Système d'alertes sécurité class SecurityMonitor { constructor(logger, alertService) { this.logger = logger; this.alertService = alertService; this.suspiciousActivity = new Map(); // Cache pour patterns } // Middleware de monitoring monitor() { return (req, res, next) => { const startTime = Date.now(); // Capturer la réponse pour analyse const originalSend = res.send; res.send = function(data) { const responseTime = Date.now() - startTime; // Analyser pattern d'attaque potentiel this.analyzeRequest(req, res, responseTime, data); originalSend.call(this, data); }.bind(this); next(); }; } analyzeRequest(req, res, responseTime, responseData) { const patterns = [ this.detectSQLInjection(req), this.detectXSSAttempt(req), this.detectDirectoryTraversal(req), this.detectBruteForce(req, res), this.detectScraping(req, responseTime) ]; const threats = patterns.filter(p => p.detected); if (threats.length > 0) { this.handleSecurityThreat(req, threats); } } detectSQLInjection(req) { const sqlPatterns = [ /(\bunion\b.*\bselect\b)|(\bselect\b.*\bunion\b)/i, /(\bdrop\b.*\btable\b)|(\btable\b.*\bdrop\b)/i, /'.*(\bor\b|\band\b).*'/i, /\b(exec|execute|sp_|xp_)\b/i ]; const inputs = [ ...Object.values(req.query || {}), ...Object.values(req.body || {}), req.url ].join(' '); const detected = sqlPatterns.some(pattern => pattern.test(inputs)); return { type: 'sql_injection', detected, severity: 'high', evidence: detected ? inputs : null }; } detectBruteForce(req, res) { if (!req.url.includes('/auth/login')) return { detected: false }; const identifier = req.ip; const key = `bf:${identifier}`; let attempts = this.suspiciousActivity.get(key) || 0; if (res.statusCode === 401) { attempts++; this.suspiciousActivity.set(key, attempts); // Nettoyer après 15 minutes setTimeout(() => { this.suspiciousActivity.delete(key); }, 15 * 60 * 1000); } return { type: 'brute_force', detected: attempts >= 10, // 10 échecs en 15 min severity: 'medium', evidence: `${attempts} failed login attempts` }; } async handleSecurityThreat(req, threats) { const incident = { timestamp: new Date().toISOString(), ip: req.ip, userAgent: req.headers['user-agent'], url: req.url, method: req.method, threats, userId: req.userId || null }; // Log structuré this.logger.warn('Security threat detected', incident); // Alertes selon sévérité const highSeverity = threats.some(t => t.severity === 'high'); if (highSeverity) { await this.alertService.sendImmediate({ title: 'High Severity Security Alert', description: `Potential ${threats[0].type} from ${req.ip}`, incident }); } // Actions automatiques await this.takeAutomatedAction(req.ip, threats); } async takeAutomatedAction(ip, threats) { const highThreatTypes = ['sql_injection', 'xss']; const hasHighThreat = threats.some(t => highThreatTypes.includes(t.type)); if (hasHighThreat) { // Bannir temporairement l'IP await this.redis.setex(`banned:${ip}`, 3600, 'auto_ban'); // 1 heure this.logger.info('IP automatically banned', { ip, reason: 'high_threat' }); } } } Plan de sécurisation progressive Phase 1 : Fondations (Semaines 1-2) Audit de base : checklist sécurité rapide HTTPS partout : certificats SSL/TLS, HSTS Mots de passe : hashing sécurisé (scrypt/bcrypt) Variables d’environnement : externaliser tous les secrets Headers sécurité : CSP, X-Frame-Options, etc. Phase 2 : Authentification (Semaines 3-4) Sessions sécurisées : configuration cookies, expiration JWT robuste : refresh tokens, révocation Rate limiting : protection brute force Validation entrées : sanitisation côté serveur Monitoring basique : logs tentatives suspectes Phase 3 : Protection avancée (Mois 2) Chiffrement données : champs sensibles en base Backup sécurisés : chiffrement, rotation Dependency scanning : audit vulnérabilités npm RBAC : permissions granulaires Incident response : procédures documentées Phase 4 : Excellence (Mois 3+) Pentest : audit externe ou interne Compliance : GDPR, SOC2 selon besoins Security training : formation équipe Bug bounty : programme de divulgation responsable Continuous monitoring : détection temps réel Conclusion La sécurité parfaite n’existe pas, mais une sécurité proportionnée et évolutive est à la portée de toute équipe. L’objectif n’est pas de se protéger contre toutes les attaques possibles, mais contre les attaques probables dans votre contexte.
...