PROFIL / INDEX© Clément Depernet / 2026

← Retour à la liste

05 déc. 20252 min de lecture

Cheat sheet — Express : durcir une API (Helmet, CORS, rate limit, validation)

Snippets prêts à adapter pour sécuriser une API Express : Helmet, CORS en allowlist, rate limiting, validation des entrées, bcrypt, JWT, prévention d'injection de commande et SQL, middleware d'erreurs.

intermediate#api#express#nodejs#securite#cheatsheet#web

Mis à jour le 23 sept. 2026

À adapter, ne pas copier tel quel en production sans revue.

Installation

npm install express helmet cors express-rate-limit validator \
  password-validator bcryptjs jsonwebtoken dotenv

Squelette d’API durcie

const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const rateLimit = require("express-rate-limit");

const app = express();

app.disable("x-powered-by");            // masque la techno serveur
app.use(helmet());                      // en-têtes de sécurité par défaut
app.use(express.json({ limit: "10kb" })); // limite la taille du corps

Helmet (en-têtes de sécurité)

app.use(helmet());

// Personnalisation (ex. CSP pour une API renvoyant du JSON)
app.use(helmet({
  contentSecurityPolicy: { directives: { defaultSrc: ["'none'"] } },
  hsts: { maxAge: 31536000, includeSubDomains: true },
}));

En-têtes clés posés par Helmet : X-Content-Type-Options: nosniff, X-Frame-Options, Strict-Transport-Security, suppression de X-Powered-By.

CORS (à restreindre !)

// MAUVAIS — ouvre l'API à tout le monde
app.use(cors({ origin: "*", credentials: true }));

// BON — liste d'autorisation explicite
const allowlist = ["https://app.example.com", "http://localhost:4000"];
app.use(cors({
  origin: (origin, cb) => cb(null, !origin || allowlist.includes(origin)),
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
}));

Rappel : CORS est une protection navigateur, pas serveur.

Rate limiting

const rateLimit = require("express-rate-limit");

const globalLimiter = rateLimit({
  windowMs: 60 * 1000, max: 100,
  standardHeaders: true, legacyHeaders: false,
  message: { error: "Trop de requêtes" },
});
app.use(globalLimiter);

// Limiteur strict anti-brute-force sur le login
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, standardHeaders: true, legacyHeaders: false });
app.post("/login", loginLimiter, loginHandler);

Dépassement → 429 Too Many Requests. Store en mémoire par défaut → utiliser rate-limit-redis en multi-instances.

Validation et normalisation des entrées

const validator = require("validator");

const email = validator.trim(req.body.email ?? "");
if (!validator.isEmail(email)) return res.status(400).json({ error: "Email invalide" });
const emailNorm = validator.normalizeEmail(email);

// Politique de mot de passe
const PasswordValidator = require("password-validator");
const schema = new PasswordValidator();
schema.is().min(12).is().max(128).has().uppercase().has().lowercase().has().digits(1).has().symbols(1).has().not().spaces();
const failures = schema.validate(req.body.password, { details: true });
if (failures.length) return res.status(400).json({ error: "Mot de passe trop faible", details: failures.map(f => f.message) });

Hachage de mot de passe (bcrypt)

const bcrypt = require("bcryptjs");
const hash = await bcrypt.hash(password, 12); // stocker le hash, jamais le clair
const ok = await bcrypt.compare(password, hash); // à la connexion

JWT

const jwt = require("jsonwebtoken");
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "1h" });

function authenticateToken(req, res, next) {
  const token = req.header("Authorization")?.split(" ")[1];
  if (!token) return res.sendStatus(401);
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user; next();
  });
}

Secret hors du code (.env + dotenv + .gitignore). Générer : node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".

Éviter l’injection de commande

const { spawn } = require("child_process");
// JAMAIS exec(`ping ${host}`) — passe par un shell.
const p = spawn("ping", ["-c", "4", "--", host]); // tableau d'arguments, pas de shell

Requêtes SQL préparées

db.get("SELECT id, name, email FROM users WHERE email = ?", [email], cb); // paramètre = donnée
// JAMAIS "... WHERE email = '" + email + "'"

Middleware d’erreurs (ne pas fuiter l’interne)

app.use((err, req, res, next) => {
  if (err.type === "entity.parse.failed") return res.status(400).json({ error: "JSON invalide" });
  console.error(err);                                   // détail côté serveur
  res.status(500).json({ error: "Erreur interne" });    // message générique côté client
});
SEC / OPS / CD-01

Astro + Tailwind, conteneurisé, servi derrière Caddy · Inspiration · Thème sonore : Severance (Theodore Shapiro) · Trop mignon le favicon, non ?

PRÊT / CD-01