Blindez une API Node en production : CORS correct, rate limiting, en-têtes de sécurité et parades aux principaux risques OWASP API.
Ouvrir cette leçon dans KodokonL'OWASP maintient un top 10 dédié aux API. Les trois risques dominants : BOLA (Broken Object Level Authorization - accéder à l'objet d'un autre utilisateur en changeant un id), Broken Authentication (tokens mal validés, secrets faibles) et Unrestricted Resource Consumption (aucune limite de débit, de taille ou de temps). Remarquez qu'aucun des trois ne se corrige en installant une bibliothèque : ce sont des propriétés de votre conception. Commençons par les mécanismes transverses : CORS, rate limiting et en-têtes.
import { createServer } from "node:http";
const allowed = new Set(["https://app.example.com"]);
const server = createServer((req, res) => {
const origin = req.headers.origin;
if (origin && allowed.has(origin)) {
res.setHeader(
"Access-Control-Allow-Origin",
origin,
);
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader(
"Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE",
);
res.setHeader(
"Access-Control-Allow-Headers",
"content-type,authorization",
);
res.setHeader("Access-Control-Max-Age", "86400");
res.writeHead(204).end();
return;
}
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
});
server.listen(3000);Comprenez bien ce que CORS n'est pas : une protection du serveur. curl ou un backend tiers ignorent totalement ces en-têtes ; seul le navigateur les applique, pour protéger ses utilisateurs. Trois règles d'expert : renvoyez l'origine exacte depuis une liste blanche (jamais d'écho aveugle de l'en-tête Origin), ajoutez Vary: Origin pour ne pas empoisonner les caches intermédiaires, et sachez que le joker * est refusé par les navigateurs dès que la requête transporte des cookies (credentials).
const WINDOW_MS = 60_000;
const LIMIT = 100;
const hits = new Map();
function check(ip) {
const now = Date.now();
const entry = hits.get(ip);
if (!entry || now - entry.start >= WINDOW_MS) {
hits.set(ip, { start: now, count: 1 });
return { ok: true, remaining: LIMIT - 1 };
}
entry.count += 1;
const remaining = Math.max(LIMIT - entry.count, 0);
return { ok: entry.count <= LIMIT, remaining };
}
console.log(check("203.0.113.7"));Les en-têtes de sécurité coûtent quelques lignes et bloquent des classes entières d'attaques : HSTS force le HTTPS pour les visites futures, X-Content-Type-Options: nosniff empêche le navigateur de deviner un type MIME, et une CSP restrictive (default-src 'none') neutralise l'exploitation d'une réponse API affichée par erreur dans un navigateur. Supprimez aussi tout en-tête qui révèle votre stack : Express ajoute X-Powered-By par défaut - désactivez-le avec app.disable('x-powered-by').
function setSecurityHeaders(res) {
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains",
);
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'",
);
res.setHeader("Cache-Control", "no-store");
}
export { setSecurityHeaders };