Durcissement de la sécurité NestJS + Next.js : la configuration de production
La configuration de sécurité concrète que nous livrons en production pour un backend NestJS et un frontend Next.js — standards helmet, CSP, rate limits, HSTS et la seule erreur d'autorisation que nous avons vu trois équipes répéter.
La sécurité par défaut dans NestJS et Next.js est plus mince que la plupart des tutoriels ne l'admettent. Helmet est livré avec des défauts raisonnables mais insuffisants pour des acheteurs B2B en 2026, le rate limiter a besoin d'un stockage backé par Redis pour être honnête dans un déploiement multi-répliques, et le pattern de guard d'autorisation que tout le monde utilise clé en main a un bug subtil que trois de nos clients ont livré en production. Cet article est la configuration que nous livrons.
Backend : durcissement NestJS
Helmet, mais pas les défauts
L'appel helmet() standard pose un petit ensemble de headers, mais les défauts ne sont pas assez agressifs pour un public B2B. La configuration que nous livrons :
- HSTS avec
maxAge: 63072000(2 ans),includeSubDomains: true,preload: true. Quelque chose de plus court est un signal vers l'acheteur que vous allez rotationner le cert et casser leur client pinné. - frameguard avec
action: « deny »** — strictementDENY, pasSAMEORIGIN. Il n'y a aucune UI dans notre app qui ait besoin qu'une autre la frame. - referrerPolicy avec
policy: « strict-origin-when-cross-origin »** — leak l'origine nue sur la navigation cross-origin, rien de plus. - Content-Security-Policy avec un strict
default-src 'self', puis une liste explicite de sources d'image/script/connect approuvées. La CSP est le header le plus important pour la défense en profondeur ; accordez-la avant de livrer, pas après une revue de sécurité.
Throttler, avec stockage backé par Redis
Le @nestjs/throttler de NestJS est livré avec un stockage en mémoire par défaut. C'est très bien en dev et dangereux en production : chaque réplique a son propre compteur, et un attaquant peut envoyer répliques × limite requêtes avant que l'une d'entre elles ne déclenche le throttle.
Le correctif :
- Câblez
@nest-lab/throttler-storage-redispour que le compteur soit partagé à travers les répliques via Redis. - Mettez l'URI de stockage sur la même Redis que votre cache. Le throttler d'auth partage le goulot d'étranglement avec le reste de l'app, mais c'est correct — le chemin d'auth est où l'abus se concentre réellement.
Protection brute-force, séparée du rate limit
Les rate limits répondent à « combien de fois une IP unique peut-elle essayer ? » La protection brute-force répond à « que se passe-t-il quand un compte voit 5 mauvais mots de passe ? » Les deux sont nécessaires. La couche brute-force :
- 5 échecs consécutifs sur le même compte → lock de 15 minutes, retourne 429 ou 423 Locked.
- Le compteur se reset à la connexion réussie.
- Les comptes inactifs ne sont jamais lockés. (La raison : un attaquant ne doit pas pouvoir locker un compte inactif pour empêcher un admin légitime de le réactiver.)
- Timing égalisé : bcrypt.compare tourne contre un dummy-hash même quand l'email n'existe pas, pour que le temps de réponse ne fuite pas d'énumération d'utilisateurs.
Erreur de guard d'autorisation
L'erreur que nous avons vue trois fois dans trois codebases différentes :
// DANGEREUX — lit le rôle depuis un header que le client peut poser.
const role = request.headers[« x-role »];
if (!requiredRoles.includes(role)) throw new ForbiddenException();
Le correctif :
// CORRECT — lit le rôle depuis le JWT que le serveur a déjà vérifié.
const role = request.user.role;
if (!requiredRoles.includes(role)) throw new ForbiddenException();
Le pattern du header x-role bypasse le système de rôles entier au moment où un développeur oublie de coller le JWT à une requête downstream. C'est un codepath qui compile, se déploie et désescalade silencieusement. Centralisez la lecture du rôle sur request.user (rempli par la stratégie JWT) et la catégorie de bug disparaît.
Validation d'entrée
class-validator avec la ValidationPipe globale est le défaut. Le hors-défaut qui compte :
whitelist: trueetforbidNonWhitelisted: true. Si le client envoie un champ qui n'est pas dans le DTO, rejetez la requête. Défense contre la pollution de prototype et la dérive de DTO accidentelle.- Un FeatureFlag sur la validation Joi/JSON-Schema pour les endpoints où les règles métier dépendent de l'environnement, pas du shape de la requête.
Frontend : durcissement Next.js
Headers via middleware
Le middleware Next.js tourne sur chaque requête avant la page. Utilisez-le pour les headers qui doivent être posés sur la réponse HTML :
- Content-Security-Policy (echo la même policy que le backend, avec
frame-ancestors 'none'ajouté). - Strict-Transport-Security (HSTS, correspondant au backend, avec
max-age=63072000). - X-Content-Type-Options: nosniff.
- Referrer-Policy: strict-origin-when-cross-origin.
- Permissions-Policy: seulement les features que nous utilisons réellement (
camera=(self),microphone=(self)pour Tolky ; rien d'autre sur la surface marketing).
Optimisation d'image, remote-patterns et le piège de dérive next.config
images.remotePatterns est l'allowlist pour l'optimiseur next/image. Le piège :
- Un développeur ajoute un nouveau domaine d'image, déploie et oublie.
- Le plugin next-intl tourne à runtime et remplace le bloc
images— laisse vos remotePatterns custom tomber silencieusement. - Chaque image de ce domaine commence à retourner 400 depuis l'optimiseur.
Le correctif est un garde runtime qui réapplique la config et un Dockerfile qui copie explicitement la next.config vers la stage runner. Le garde attrape la dérive en production ; la copie Dockerfile l'attrape au build. Deux couches pour la même raison : ne laissez pas le plugin next-intl manger votre politique d'optimisation d'image.
Rate-limiter les endpoints d'auth depuis le frontend
Si vous pouvez, rate-limitez sur le backend. Si vous ne pouvez pas (par ex. un fournisseur d'auth externe), rate-limitez sur le middleware frontend pour ralentir les tentatives de credential-stuffing à un débit tolérable. Deux couches.
L'erreur partagée : log injection
L'erreur que nous avons vue dans chaque projet NestJS + Next.js que nous avons audité : des messages de log qui interpolent de l'input utilisateur sans échapper. Un champ de recherche qui log Search query: ${req.query.q} devient un vecteur d'injection de log au moment où un attaquant envoie q=\n[FAKE LOG ENTRY]\n.
Le correctif est une ligne : échappez tout input utilisateur qui atterrit dans une ligne de log. Quasiment chaque library de logging a un pattern escape ou structured-logging qui gère cela pour vous. Utilisez le pattern.
Le principe de clôture
La sécurité n'est pas une feature. C'est l'absence d'une classe de bugs. La configuration ci-dessus élimine les classes que nous avons vu livrées en production l'année dernière. La liste grandira. Pinnez cet article, revisez-le trimestriellement et livrez une baseline durcie.
