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 = {
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#x27;',
        '&': '&amp;'
      };
      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.

Commencez par les fondations solides : HTTPS, authentification robuste, validation des entrées, monitoring basique. Ces mesures couvrent 80% des risques avec 20% de l’effort.

La sécurité n’est pas un état mais un processus. Chaque nouvelle fonctionnalité, chaque évolution d’architecture doit intégrer la dimension sécurité dès la conception.

Et rappelez-vous : une équipe sensibilisée vaut mieux que mille outils sophistiqués. Investissez dans la formation et la culture sécurité de vos équipes.

Quelle sera votre première action pour renforcer la sécurité de votre application ?