Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    Sécurité

    Sécurité web: checklists essentielles 2025

    Antonin Nhek
    6 min de lecture

    CSRF, XSS, headers, secrets: le minimum vital à industrialiser.

    Sécurité web: checklists essentielles 2025 - Sécurité | IT INNOVE
    Sécurité web: checklists essentielles 2025 - Sécurité

    Sécurité web: checklists essentielles 2025

    La sécurité doit être automatisée
    politiques de contenu strictes, rotation des secrets, validations côté serveur et scans réguliers. Nous partageons notre checklist et nos outils préférés.

    L'état de la sécurité web en 2025

    Les menaces qui évoluent

    **Statistiques alarmantes
    **
    • 95% des failles exploitées sont connues depuis >1 an
    • $4.45M coût moyen d'une brèche de données en 2024
    • 200% d'augmentation des attaques sur APIs
    • 43% des PME visées par des cyberattaques

    Notre approche "Security by Design"

    Nous intégrons la sécurité dès la conception sur tous nos projets :

    • 100% de nos projets auditées automatiquement
    • Zero-day security patches appliqués sous 48h
    • Penetration testing sur chaque mise en prod
    • Security training continue des équipes

    1. Protection contre XSS (Cross-Site Scripting)

    Content Security Policy (CSP) stricte

    html
    <!-- CSP progressive pour bloquer XSS -->
    <meta http-equiv="Content-Security-Policy" content="
      default-src 'self';
      script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https:;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.stripe.com;
      frame-ancestors 'none';
      base-uri 'self';
      form-action 'self';
    ">
    **CSP en mode report pour debug
    **
    javascript
    // Node.js - CSP avec reporting
    app.use((req, res, next) => {
      res.setHeader(
        'Content-Security-Policy-Report-Only',
        "default-src 'self'; report-uri /csp-violations"
      )
      next()
    })
    
    // Handler des violations CSP
    app.post('/csp-violations', express.json(), (req, res) => {
      const violation = req.body['csp-report']
      console.error('CSP Violation:', {
        documentURI: violation['document-uri'],
        violatedDirective: violation['violated-directive'],
        blockedURI: violation['blocked-uri'],
        userAgent: req.headers['user-agent']
      })
      
      // Alerter en cas de violations répétées
      if (violationCount[violation['blocked-uri']] > 10) {
        sendSecurityAlert('CSP violations répétées', violation)
      }
      
      res.sendStatus(204)
    })

    Sanitisation et validation input

    javascript
    // Validation et sanitisation côté serveur
    import DOMPurify from 'isomorphic-dompurify'
    import validator from 'validator'
    
    const sanitizeInput = (input, type) => {
      switch (type) {
        case 'html':
          return DOMPurify.sanitize(input, {
            ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
            ALLOWED_ATTR: []
          })
          
        case 'email':
          return validator.isEmail(input) ? validator.normalizeEmail(input) : null
          
        case 'url':
          return validator.isURL(input) ? input : null
          
        case 'text':
          return validator.escape(input)
          
        default:
          return validator.escape(input)
      }
    }
    
    // Middleware de validation
    const validateBody = (schema) => (req, res, next) => {
      const errors = []
      
      Object.keys(schema).forEach(field => {
        const value = req.body[field]
        const rules = schema[field]
        
        if (rules.required && !value) {
          errors.push(`${field} is required`)
          return
        }
        
        if (value) {
          req.body[field] = sanitizeInput(value, rules.type)
          
          if (rules.maxLength && value.length > rules.maxLength) {
            errors.push(`${field} too long`)
          }
        }
      })
      
      if (errors.length > 0) {
        return res.status(400).json({ errors })
      }
      
      next()
    }
    
    // Usage
    app.post('/comments', validateBody({
      content: { type: 'html', required: true, maxLength: 1000 },
      email: { type: 'email', required: true }
    }), (req, res) => {
      // Input sécurisé et validé
      saveComment(req.body)
      res.json({ success: true })
    })

    2. Protection CSRF (Cross-Site Request Forgery)

    Tokens CSRF dynamiques

    javascript
    // Génération et validation tokens CSRF
    import crypto from 'crypto'
    
    const csrfTokens = new Map() // En production: Redis/DB
    
    const generateCSRFToken = (sessionId) => {
      const token = crypto.randomBytes(32).toString('hex')
      const expiry = Date.now() + (60 * 60 * 1000) // 1h
      
      csrfTokens.set(sessionId, { token, expiry })
      return token
    }
    
    const validateCSRFToken = (sessionId, submittedToken) => {
      const stored = csrfTokens.get(sessionId)
      
      if (!stored || stored.expiry < Date.now()) {
        csrfTokens.delete(sessionId)
        return false
      }
      
      return stored.token === submittedToken
    }
    
    // Middleware CSRF
    const csrfProtection = (req, res, next) => {
      if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
        const sessionId = req.session.id
        const token = req.headers['x-csrf-token'] || req.body._csrf
        
        if (!validateCSRFToken(sessionId, token)) {
          return res.status(403).json({ error: 'Invalid CSRF token' })
        }
      }
      next()
    }
    
    // Route pour obtenir un token
    app.get('/csrf-token', (req, res) => {
      const token = generateCSRFToken(req.session.id)
      res.json({ csrfToken: token })
    })
    
    // Protection sur routes sensibles
    app.use('/api', csrfProtection)

    SameSite cookies + Double Submit

    javascript
    // Configuration cookies sécurisée
    app.use(session({
      name: 'sessionId', // Pas de nom par défaut
      secret: process.env.SESSION_SECRET,
      cookie: {
        httpOnly: true,        // Pas d'accès JS
        secure: process.env.NODE_ENV === 'production', // HTTPS only
        sameSite: 'strict',    // Protection CSRF
        maxAge: 24 * 60 * 60 * 1000, // 24h
        domain: '.monsite.com' // Sous-domaines autorisés
      },
      resave: false,
      saveUninitialized: false,
      store: new RedisStore({
        client: redisClient,
        prefix: 'session:'
      })
    }))
    
    // Double Submit Pattern pour APIs
    const doubleSubmitCSRF = (req, res, next) => {
      const tokenHeader = req.headers['x-csrf-token']
      const tokenCookie = req.cookies['csrf-token']
      
      if (tokenHeader && tokenHeader === tokenCookie) {
        return next()
      }
      
      res.status(403).json({ error: 'CSRF token mismatch' })
    }

    3. Gestion sécurisée des secrets

    Variables d'environnement et rotation

    javascript
    // Configuration secrets avec validation
    import { z } from 'zod'
    
    const envSchema = z.object({
      DATABASE_URL: z.string().url(),
      JWT_SECRET: z.string().min(32),
      API_KEY: z.string().min(20),
      ENCRYPTION_KEY: z.string().length(32),
      NODE_ENV: z.enum(['development', 'staging', 'production'])
    })
    
    // Validation au démarrage
    const config = envSchema.parse(process.env)
    
    // Chiffrement secrets sensibles
    import crypto from 'crypto'
    
    class SecretManager {
      constructor() {
        this.algorithm = 'aes-256-gcm'
        this.key = Buffer.from(config.ENCRYPTION_KEY, 'hex')
      }
      
      encrypt(text) {
        const iv = crypto.randomBytes(16)
        const cipher = crypto.createCipher(this.algorithm, this.key, iv)
        
        let encrypted = cipher.update(text, 'utf8', 'hex')
        encrypted += cipher.final('hex')
        
        const authTag = cipher.getAuthTag()
        
        return {
          encrypted,
          iv: iv.toString('hex'),
          authTag: authTag.toString('hex')
        }
      }
      
      decrypt(encryptedData) {
        const decipher = crypto.createDecipher(
          this.algorithm, 
          this.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 decrypted
      }
    }
    
    // Usage pour secrets en base
    const secretManager = new SecretManager()
    const encryptedApiKey = secretManager.encrypt(apiKey)

    Rotation automatique des secrets

    javascript
    // Script rotation automatique (cron daily)
    const rotateSecrets = async () => {
      const secrets = [
        { name: 'JWT_SECRET', length: 32 },
        { name: 'API_KEY', length: 24 },
        { name: 'CSRF_SECRET', length: 32 }
      ]
      
      for (const secret of secrets) {
        const newSecret = crypto.randomBytes(secret.length).toString('hex')
        
        // 1. Créer nouveau secret
        await vault.createSecret(`${secret.name}_NEW`, newSecret)
        
        // 2. Déployer avec ancien + nouveau (double validation)
        await deployWithBothSecrets(secret.name)
        
        // 3. Attendre propagation (5min)
        await sleep(5 * 60 * 1000)
        
        // 4. Supprimer ancien secret
        await vault.deleteSecret(secret.name)
        await vault.renameSecret(`${secret.name}_NEW`, secret.name)
        
        console.log(`✅ ${secret.name} rotated successfully`)
      }
    }
    
    // Monitoring expiration secrets
    const checkSecretExpiry = async () => {
      const secrets = await vault.listSecrets()
      const expiringSoon = secrets.filter(s => 
        s.expiryDate && s.expiryDate - Date.now() < 7 * 24 * 60 * 60 * 1000
      )
      
      if (expiringSoon.length > 0) {
        await sendAlert('Secrets expiring soon', expiringSoon)
      }
    }

    4. Headers de sécurité essentiels

    Configuration complète des headers

    javascript
    // Headers sécurisés avec helmet.js
    import helmet from 'helmet'
    
    app.use(helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
          fontSrc: ["'self'", "https://fonts.gstatic.com"],
          imgSrc: ["'self'", "data:", "https:"],
          scriptSrc: ["'self'"],
          connectSrc: ["'self'", "https://api.stripe.com"]
        },
      },
      
      hsts: {
        maxAge: 31536000, // 1 an
        includeSubDomains: true,
        preload: true
      },
      
      frameguard: { action: 'deny' }, // Pas d'iframe
      
      noSniff: true, // Pas de MIME sniffing
      
      xssFilter: true, // XSS protection native
      
      referrerPolicy: { policy: 'same-origin' }
    }))
    
    // Headers personnalisés
    app.use((req, res, next) => {
      // Supprimer headers révélateurs
      res.removeHeader('X-Powered-By')
      res.setHeader('Server', 'WebServer') // Générique
      
      // Headers permissions API
      res.setHeader('Permissions-Policy', 
        'camera=(), microphone=(), geolocation=self'
      )
      
      // Cross-Origin Policies
      res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
      res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
      
      next()
    })

    HTTPS et redirections sécurisées

    javascript
    // Force HTTPS en production
    const forceHTTPS = (req, res, next) => {
      if (process.env.NODE_ENV === 'production' && req.header('x-forwarded-proto') !== 'https') {
        return res.redirect(301, `https://${req.get('Host')}${req.url}`)
      }
      next()
    }
    
    // Préload HSTS
    app.get('/', (req, res) => {
      res.setHeader('Strict-Transport-Security', 
        'max-age=31536000; includeSubDomains; preload'
      )
      res.send('Hello Secure World!')
    })

    5. Authentification et autorisation

    JWT sécurisé avec refresh tokens

    javascript
    // Génération JWT avec claims minimaux
    import jwt from 'jsonwebtoken'
    
    const generateTokens = (user) => {
      const payload = {
        sub: user.id,
        role: user.role,
        iat: Math.floor(Date.now() / 1000)
      }
      
      const accessToken = jwt.sign(payload, config.JWT_SECRET, {
        expiresIn: '15m', // Court pour limiter exposition
        issuer: 'monapp.com',
        audience: 'api.monapp.com'
      })
      
      const refreshToken = crypto.randomBytes(32).toString('hex')
      
      // Stocker refresh token avec expiration
      await redis.setex(`refresh:${user.id}`, 7 * 24 * 60 * 60, refreshToken)
      
      return { accessToken, refreshToken }
    }
    
    // Validation JWT stricte
    const validateToken = (req, res, next) => {
      const authHeader = req.headers.authorization
      const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
      
      if (!token) {
        return res.status(401).json({ error: 'Token required' })
      }
      
      try {
        const decoded = jwt.verify(token, config.JWT_SECRET, {
          issuer: 'monapp.com',
          audience: 'api.monapp.com',
          clockTolerance: 60 // 1min de tolérance
        })
        
        req.user = decoded
        next()
      } catch (error) {
        if (error.name === 'TokenExpiredError') {
          return res.status(401).json({ error: 'Token expired' })
        }
        return res.status(401).json({ error: 'Invalid token' })
      }
    }

    Contrôle d'accès granulaire (RBAC)

    javascript
    // Système de permissions flexible
    const permissions = {
      'user:read': ['admin', 'moderator', 'user'],
      'user:write': ['admin', 'moderator'],
      'user:delete': ['admin'],
      'post:create': ['admin', 'author'],
      'post:publish': ['admin', 'editor']
    }
    
    const hasPermission = (userRole, permission) => {
      return permissions[permission]?.includes(userRole) ?? false
    }
    
    // Middleware autorisation
    const requirePermission = (permission) => (req, res, next) => {
      if (!req.user) {
        return res.status(401).json({ error: 'Authentication required' })
      }
      
      if (!hasPermission(req.user.role, permission)) {
        return res.status(403).json({ 
          error: 'Insufficient permissions',
          required: permission,
          current: req.user.role
        })
      }
      
      next()
    }
    
    // Usage sur routes
    app.get('/admin/users', validateToken, requirePermission('user:read'), getUserList)
    app.delete('/admin/users/:id', validateToken, requirePermission('user:delete'), deleteUser)

    6. Monitoring et détection d'intrusion

    Logging sécurisé structuré

    javascript
    // Logger sécurisé avec Winston
    import winston from 'winston'
    
    const securityLogger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json(),
        winston.format.printf(({ timestamp, level, message, ...meta }) => {
          // Masquer données sensibles
          const sanitizedMeta = { ...meta }
          delete sanitizedMeta.password
          delete sanitizedMeta.creditCard
          
          return JSON.stringify({
            timestamp,
            level,
            message,
            ...sanitizedMeta
          })
        })
      ),
      transports: [
        new winston.transports.File({ 
          filename: 'security.log',
          maxsize: 10485760, // 10MB
          maxFiles: 5
        })
      ]
    })
    
    // Événements de sécurité à logger
    const logSecurityEvent = (event, details) => {
      securityLogger.warn('Security Event', {
        event,
        ip: details.ip,
        userAgent: details.userAgent,
        userId: details.userId,
        timestamp: new Date().toISOString(),
        severity: details.severity || 'medium'
      })
    }
    
    // Détection tentatives de brute force
    const loginAttempts = new Map()
    
    const checkBruteForce = (ip) => {
      const attempts = loginAttempts.get(ip) || { count: 0, firstAttempt: Date.now() }
      const timeWindow = 15 * 60 * 1000 // 15 minutes
      
      if (Date.now() - attempts.firstAttempt > timeWindow) {
        // Reset après fenêtre de temps
        attempts.count = 1
        attempts.firstAttempt = Date.now()
      } else {
        attempts.count++
      }
      
      loginAttempts.set(ip, attempts)
      
      if (attempts.count > 5) {
        logSecurityEvent('brute_force_detected', {
          ip,
          attempts: attempts.count,
          severity: 'high'
        })
        return false // Bloquer
      }
      
      return true
    }

    Alertes automatisées

    javascript
    // Système d'alertes sécurité
    const alertThresholds = {
      failed_logins: 10, // par heure
      csp_violations: 5, // par heure  
      rate_limit_hits: 100, // par heure
      suspicious_requests: 20 // par heure
    }
    
    const checkSecurityMetrics = async () => {
      const lastHour = Date.now() - (60 * 60 * 1000)
      
      const metrics = await getSecurityMetrics(lastHour)
      
      Object.keys(alertThresholds).forEach(metric => {
        if (metrics[metric] > alertThresholds[metric]) {
          sendSecurityAlert(metric, metrics[metric])
        }
      })
    }
    
    const sendSecurityAlert = async (type, value) => {
      const alert = {
        type,
        value,
        threshold: alertThresholds[type],
        timestamp: new Date().toISOString(),
        severity: value > (alertThresholds[type] * 2) ? 'critical' : 'warning'
      }
      
      // Slack, email, SMS selon gravité
      if (alert.severity === 'critical') {
        await sendSMSAlert(alert)
      }
      await sendSlackAlert(alert)
      
      securityLogger.error('Security Alert', alert)
    }
    
    // Surveillance en temps réel
    setInterval(checkSecurityMetrics, 5 * 60 * 1000) // Toutes les 5min

    7. Checklist sécurité par environnement

    Développement

    • [ ] Variables d'environnement locales sécurisées
    • [ ] Pas de secrets dans le code/git
    • [ ] HTTPS même en local (mkcert)
    • [ ] CSP en mode report-only pour debug

    Staging

    • [ ] Réplication configuration production
    • [ ] Tests d'intrusion automatisés
    • [ ] Validation certificates SSL/TLS
    • [ ] Audit dépendances (npm audit)

    Production

    • [ ] Tous les headers sécurisés activés
    • [ ] Monitoring sécurité 24/7
    • [ ] Backup chiffrés et testés
    • [ ] Plan de réponse incident documenté

    Outils recommandés 2025

    Analyse statique

    • Snyk : Vulnérabilités dépendances
    • SonarQube : Code quality + sécurité
    • Semgrep : SAST patterns custom
    • GitLeaks : Détection secrets git

    Tests dynamiques

    • OWASP ZAP : Scan vulnerabilités web
    • Nuclei : Tests automatisés rapides
    • Burp Suite : Pentest manuel professionnel

    Monitoring runtime

    • Datadog Security : SIEM + détection temps réel
    • Falco : Détection comportements anormaux
    • Crowdsec : Protection collaborative

    ROI de la sécurité

    Coût d'une implémentation complète

    • Audit initial : 5-8k€
    • Mise en conformité : 15-25k€
    • Monitoring annuel : 8-12k€
    • Total première année : 28-45k€

    Coûts évités

    • Brèche de données : 150k-4M€ selon secteur
    • Downtime : 5-50k€ par heure
    • Amendes RGPD : jusqu'à 4% CA
    • Image de marque : inestimable

    ROI typique

    • 6-12 mois de retour sur investissement
    • 85% de réduction des incidents
    • Confiance client renforcée

    Votre site est-il vraiment sécurisé contre les menaces 2025 ? Nos experts peuvent auditer votre sécurité et identifier les vulnérabilités critiques. Contactez-nous pour un audit sécurité gratuit et protégez votre business dès maintenant.

    Questions fréquentes