Ouvrez l'onglet Network de votre navigateur sur votre site en production et regardez les headers de réponse. Si vous n'y voyez ni Content-Security-Policy, ni Strict-Transport-Security, votre site tourne avec les défenses par défaut du navigateur — c'est-à-dire pas grand-chose. La bonne nouvelle : dans Next.js, corriger ça prend moins d'une heure, et ça se fait entièrement en configuration.
Dans cet article, je passe en revue les headers de sécurité qui comptent vraiment, comment les configurer proprement dans Next.js (y compris la fameuse CSP avec nonce, la partie qui fait transpirer tout le monde), et comment vérifier que tout fonctionne.
Pourquoi les headers de sécurité ne sont pas optionnels
Les headers de sécurité sont des instructions que votre serveur envoie au navigateur pour lui dire comment se comporter : quels scripts il a le droit d'exécuter, si la page peut être affichée dans une iframe, si la connexion doit obligatoirement passer par HTTPS…
Sans eux, vous êtes exposé à trois familles d'attaques bien connues :
Le XSS (Cross-Site Scripting) : un attaquant injecte du JavaScript malveillant dans votre page — via un champ de formulaire mal échappé, un paramètre d'URL, un commentaire. Ce script s'exécute avec les droits de vos utilisateurs : vol de session, exfiltration de données, redirection vers un site de phishing. La CSP est votre dernière ligne de défense quand l'échappement a échoué.
Le clickjacking : votre site est chargé dans une iframe invisible sur un site malveillant, et l'utilisateur clique sur des éléments de votre interface sans le savoir. X-Frame-Options et frame-ancestors bloquent ça net.
Le downgrade HTTPS → HTTP : un attaquant en position d'intercepter le trafic (Wi-Fi public, par exemple) force la connexion en HTTP pour lire les échanges en clair. HSTS rend ça impossible.
Aucun de ces headers ne remplace un code propre. Mais ils transforment une faille exploitable en tentative avortée — et c'est exactement leur rôle.
Les headers essentiels, configurés dans next.config.ts
Next.js permet de définir des headers globaux directement dans la configuration. Voici la base que je déploie sur tous mes projets :
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
// Force le HTTPS pendant 2 ans, sous-domaines inclus
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
// Interdit l'affichage du site dans une iframe
key: "X-Frame-Options",
value: "DENY",
},
{
// Empêche le navigateur de "deviner" le type MIME
key: "X-Content-Type-Options",
value: "nosniff",
},
{
// Limite les infos envoyées dans le header Referer
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
// Désactive les API navigateur inutilisées
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
];
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
export default nextConfig;
Quelques précisions sur les choix :
- HSTS avec
preload: une fois votre domaine inscrit sur la liste de préchargement HSTS, les navigateurs refuseront toute connexion HTTP avant même la première visite. Attention : c'est quasi irréversible, ne l'activez que si tout votre domaine (sous-domaines compris) est en HTTPS pour de bon. X-Frame-Options: DENY: si vous avez besoin d'intégrer votre propre site en iframe quelque part (rare), passez àSAMEORIGIN.Permissions-Policy: la liste est à adapter. Si votre site utilise la géolocalisation, retirezgeolocation=()de la liste.
Ces cinq headers couvrent déjà l'essentiel. Il en manque un — le plus puissant et le plus délicat.
La Content-Security-Policy : le boss final
La CSP dit au navigateur précisément quelles ressources il a le droit de charger et d'exécuter : scripts, styles, images, fonts, connexions. Un script injecté par XSS qui ne respecte pas la politique est purement et simplement bloqué.
Le problème classique : une CSP stricte casse tout au premier déploiement, parce que Next.js injecte des scripts inline pour l'hydratation. La mauvaise solution, qu'on voit partout, est d'ajouter 'unsafe-inline' à script-src — ce qui revient à désactiver la protection XSS de la CSP. Autant ne rien mettre.
La bonne solution : les nonces. Un nonce est une valeur aléatoire générée à chaque requête, attachée à la CSP et à chaque balise <script> légitime. Un script injecté ne connaît pas le nonce du moment, donc il est bloqué.
Dans Next.js App Router, ça se met en place dans le middleware :
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Nonce unique par requête
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, " ")
.trim();
// On transmet le nonce à Next.js via les headers de requête
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", cspHeader);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// Et on l'applique à la réponse
response.headers.set("Content-Security-Policy", cspHeader);
return response;
}
export const config = {
matcher: [
{
// On exclut les assets statiques et les prefetches
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};
Le point clé : quand Next.js détecte le header x-nonce dans la requête, il l'applique automatiquement à tous les scripts qu'il génère lors du rendu serveur. Vous n'avez rien d'autre à faire pour vos propres pages.
Décryptage des directives importantes :
'strict-dynamic': les scripts chargés par un script de confiance (porteur du nonce) sont automatiquement autorisés. Indispensable pour le chunking de Next.js, qui charge ses bundles dynamiquement.style-src 'unsafe-inline': oui, c'est un compromis. Les styles inline sont nécessaires pour beaucoup de bibliothèques (et le risque d'attaque via CSS est très inférieur à celui via script). C'est un compromis accepté par l'écosystème.frame-ancestors 'none': la version moderne deX-Frame-Options: DENY. On garde les deux pour la compatibilité.upgrade-insecure-requests: réécrit automatiquement les URLs HTTP en HTTPS dans la page.
Si vous utilisez déjà le middleware pour protéger des routes, ces logiques cohabitent très bien — j'en parle en détail dans mon article sur le middleware Next.js App Router.
Gérer les services tiers sans tout casser
Le monde réel a des analytics, des fonts externes, des iframes YouTube. Chaque service tiers doit être explicitement autorisé dans la CSP. Exemple avec des services courants :
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' blob: data: https://images.unsplash.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://vitals.vercel-insights.com;
frame-src https://www.youtube-nocookie.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`;
Deux conseils issus de mes déploiements :
Déployez d'abord en mode Report-Only. Le header Content-Security-Policy-Report-Only applique la politique sans rien bloquer : les violations sont seulement signalées dans la console du navigateur. Vous laissez tourner quelques jours, vous identifiez tout ce qui aurait cassé, vous ajustez, puis vous basculez sur le header réel. Zéro mauvaise surprise en production.
Loggez les violations. La directive report-uri (ou report-to, plus récente) envoie chaque violation à un endpoint de votre choix. Couplé à un outil comme Sentry — dont je détaille l'installation dans mon guide Sentry avec Next.js et NestJS — vous voyez en temps réel si une CSP bloque un script légitime… ou une vraie tentative d'injection.
// Ajout à la CSP pour remonter les violations
const cspWithReporting = cspHeader + " report-uri /api/csp-report;";
Et l'endpoint côté Next.js :
// app/api/csp-report/route.ts
export async function POST(request: Request) {
const report = await request.json();
console.warn("CSP Violation:", JSON.stringify(report["csp-report"]));
// En production : envoyer vers Sentry ou votre système de logs
return new Response(null, { status: 204 });
}
Vérifier que tout fonctionne
Une configuration de sécurité non testée est une configuration cassée. Trois outils pour valider :
En local, avec curl :
curl -I https://votre-site.fr | grep -iE \
"content-security|strict-transport|x-frame|x-content|referrer|permissions"
Vous devez voir chacun de vos headers dans la réponse. Si la CSP manque sur certaines pages, vérifiez le matcher de votre middleware.
Mozilla Observatory (developer.mozilla.org/en-US/observatory) : scanne votre site et attribue une note de F à A+. C'est l'outil de référence, gratuit, et le rapport explique chaque point perdu. Un site Next.js avec la configuration de cet article obtient un A ou A+.
Le test grandeur nature : ouvrez la console de votre navigateur sur votre site en production et tapez :
const s = document.createElement("script");
s.src = "https://exemple-malveillant.com/script.js";
document.body.appendChild(s);
Avec une CSP correcte, le navigateur refuse le chargement avec une erreur explicite Refused to load the script because it violates the following Content Security Policy directive. C'est exactement ce qui arriverait à un script injecté par XSS.
Les erreurs que je vois le plus souvent
'unsafe-inline' dans script-src : la plus répandue. Elle est souvent ajoutée « temporairement » pour faire passer un script tiers récalcitrant… et reste en production pendant des années. Avec elle, votre CSP ne protège plus de rien côté scripts. Prenez le temps de faire fonctionner les nonces.
Une CSP sur next.config.ts au lieu du middleware : les headers définis dans next.config.ts sont statiques — impossible d'y générer un nonce par requête. Ils sont parfaits pour HSTS ou X-Frame-Options, mais la CSP avec nonce doit vivre dans le middleware.
Oublier les environnements de preview : vos déploiements de preview (Vercel, etc.) doivent aussi être protégés, mais attention au header HSTS preload sur des domaines de test. En développement, pensez aussi que le hot reload de Next.js a besoin de 'unsafe-eval' — conditionnez votre CSP selon process.env.NODE_ENV.
Confondre sécurité front et sécurité back : les headers protègent le navigateur de vos utilisateurs, pas votre API. Côté serveur, il vous faut d'autres garde-fous — validation des entrées, authentification, et rate limiting sur votre API NestJS pour bloquer les abus.
Conclusion
Récapitulons la démarche : les headers statiques (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) se configurent en cinq minutes dans next.config.ts. La CSP, elle, se génère dans le middleware avec un nonce par requête et 'strict-dynamic', se déploie d'abord en Report-Only, puis s'active pour de bon une fois les services tiers autorisés. Enfin, on valide le tout avec Mozilla Observatory et on surveille les violations en production.
C'est un investissement d'une demi-journée qui protège durablement vos utilisateurs — et qui rassure aussi les clients exigeants : une note A+ sur un scanner de sécurité, ça se montre.
Vous avez un projet web et vous voulez un site rapide, bien référencé et sécurisé dès la conception ? N'hésitez pas à me contacter pour en discuter, ou écrivez-moi à contact@alexis-mouchon.fr.