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
});