Retour au cours

backend / nodejs

Sécurité — Helmet, rate limiting, validation

Leçon 141 exercice

Explication

Ce que vous allez apprendre

  • Configurer les en-têtes de sécurité HTTP essentiels avec Helmet
  • Configurer CORS avec une liste blanche d'origines, sans jamais combiner wildcard et cookies
  • Mettre en place un rate limiting global et un rate limiting renforcé sur les routes sensibles
  • Valider systématiquement les entrées utilisateur pour éviter les injections (NoSQL incluse)
  • Limiter la taille des requêtes pour éviter un déni de service par payload géant

Dans quel contexte ?

Une API de gestion de commandes vient d'être mise en ligne. Dès la première semaine, les logs montrent des centaines de tentatives de connexion automatisées sur /auth/login, ainsi que des requêtes envoyées depuis des sites tiers non autorisés qui tentent d'utiliser les cookies de session des utilisateurs connectés. Cette leçon regroupe les protections concrètes à mettre en place face à ce type d'abus, chacune répondant à une menace précise.

Une idée à poser avant tout

Une API exposée sur Internet accumule forcément les tentatives d'abus. Bots qui scannent les vulnérabilités connues, tentatives de connexion en force brute, requêtes malformées volontairement pour planter le serveur.

Cette leçon regroupe plusieurs protections COMPLÉMENTAIRES, chacune répondant à une menace différente. Commençons par la plus simple à mettre en place : Helmet.

De nombreuses attaques classiques sont bloquées simplement en envoyant les bons en-têtes HTTP. Clickjacking, sniffing de type MIME, connexions non chiffrées — Helmet configure ces en-têtes en une seule ligne.

Une fois ces bases posées, une deuxième protection concerne qui a le droit d'appeler ton API : CORS. Il contrôle quels sites web peuvent le faire depuis un navigateur.

Il existe un piège dangereux et fréquent ici. Combiner origin: "*" (tout le monde autorisé) avec credentials: true (cookies envoyés) expose les utilisateurs authentifiés à des attaques depuis n'importe quel site malveillant.

Piège fréquent

cors({ origin: "*", credentials: true }) est en réalité rejeté par la plupart des navigateurs modernes, mais certaines configurations mal comprises y arrivent quand même en pratique (proxy, régression de config). Le résultat est catastrophique : n'importe quel site peut faire des requêtes authentifiées au nom de l'utilisateur connecté. Utilisez toujours une liste blanche explicite d'origines.

La bonne pratique est une liste blanche explicite d'origines de confiance. Jamais un wildcard combiné à l'envoi de cookies.

Une fois CORS configuré, il reste à ralentir les abus sans pénaliser les utilisateurs légitimes : le rate limiting. Sans limite de fréquence, un attaquant peut essayer des milliers de mots de passe par seconde sur /auth/login.

Un rate limiter GLOBAL protège contre le déni de service général. Un rate limiter PLUS STRICT sur les routes sensibles, comme le login, cible spécifiquement les scénarios d'abus les plus dangereux.

ProtectionMenace ciblée
Helmetclickjacking, sniffing MIME, connexions non chiffrées
CORS (liste blanche)requêtes cross-origin non autorisées avec cookies
Rate limitingforce brute, déni de service par volume
Validation d'entréeinjection (SQL, NoSQL), données malformées

Il reste un dernier principe universel à appliquer partout : ne jamais faire confiance à l'entrée utilisateur. Toute donnée venant du client doit être validée AVANT d'être utilisée.

Un cas précis illustre bien ce risque : l'injection NoSQL. Sans filtrage, un payload comme {"email": {"$gt": ""}} peut détourner la logique d'une requête MongoDB pour contourner l'authentification — une classe d'attaques souvent méconnue de qui ne connaît que l'injection SQL classique.

Le piège fréquent à connaître pour finir : ne limiter que la fréquence des requêtes sans limiter leur TAILLE laisse la porte ouverte à un déni de service par requête géante, un seul payload de plusieurs gigaoctets pouvant suffire à saturer la mémoire du serveur.

Commandes & code

Sécurité — Helmet, rate limiting, validation

bash
npm install helmet express-rate-limit express-validator cors
js
// Helmet — headers HTTP de sécurité en une ligne
import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        objectSrc: ["'none'"],
      },
    },
    hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
  })
);
js
// CORS — autoriser explicitement les origines, jamais un wildcard en production avec credentials
import cors from "cors";

const allowedOrigins = ["https://monapp.com", "https://admin.monapp.com"];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Origine non autorisée par CORS"));
      }
    },
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE"],
  })
);
js
// Rate limiting — global et renforcé sur les routes sensibles
import rateLimit from "express-rate-limit";

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 300,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Trop de requêtes, réessayez plus tard" },
});

app.use(globalLimiter);

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 5 tentatives de connexion max par IP toutes les 15 minutes
  skipSuccessfulRequests: true, // ne compte pas les connexions réussies
});

app.post("/auth/login", loginLimiter, loginHandler);
js
// Validation d'entrée — express-validator ou Zod, jamais faire confiance au payload brut
import { body, validationResult } from "express-validator";

const validateCreateUser = [
  body("email").isEmail().normalizeEmail(),
  body("password").isLength({ min: 12 }).withMessage("12 caractères minimum"),
  body("age").optional().isInt({ min: 13, max: 120 }),
];

app.post("/users", validateCreateUser, (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

  // req.body est maintenant validé et normalisé
  res.status(201).json({ ok: true });
});
js
// Prévenir l'injection NoSQL (MongoDB) — filtrer les clés suspectes dans le payload
import mongoSanitize from "express-mongo-sanitize";

app.use(mongoSanitize()); // supprime les clés commençant par "$" ou contenant "."
// empêche des payloads du type { "email": { "$gt": "" } } de contourner l'authentification
js
// Limiter la taille des payloads pour prévenir un déni de service par requête géante
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: true, limit: "100kb" }));

Résumé

  • Helmet configure d'un coup les headers de sécurité essentiels (CSP, HSTS, X-Frame-Options...).
  • CORS avec liste blanche explicite d'origines, jamais origin: "*" combiné à credentials: true.
  • Rate limiting renforcé sur les routes sensibles (login, reset password) au-delà du seuil global.
  • Toute entrée doit être validée ET la taille des payloads limitée pour prévenir les abus.

Exercices pratiques

1 disponible
1

Mission : bloquer une vague d'attaques automatisées en pleine nuit

Objectif : Sécuriser une API exposée en configurant CORS sans faille, un rate limiting ciblé sur les routes sensibles, et une protection contre l'injection NoSQL et les payloads géants.

Contexte

Les logs de la nuit montrent trois signaux distincts sur l'API de commandes : des centaines de tentatives de connexion automatisées sur /auth/login (jusqu'à 40 par seconde depuis la même IP), une configuration CORS actuelle en origin: "*" combinée à credentials: true sur les cookies de session, et une requête isolée de 800 Mo envoyée sur POST /orders qui a fait grimper la mémoire du serveur jusqu'au crash. Aucune limite de taille de payload n'est actuellement configurée.

Résoudre l’exercice →