- Sécurité
- Web
- Next.js
Les en-têtes de sécurité expliqués : CSP, HSTS et les autres
Guide pratique de la CSP avec nonces et strict-dynamic, de HSTS et du preload, de frame-ancestors, Referrer-Policy, Permissions-Policy et COOP, config Next.js.
· 7 min de lecture · Lina Source LLC
Les en-têtes de sécurité sont des instructions que votre serveur donne au navigateur : n’exécute que ces scripts, ne communique avec moi qu’en HTTPS, n’autorise pas d’autres sites à afficher cette page dans un cadre. Ils ne corrigent pas les bugs de votre code, mais ils limitent ce qu’un attaquant peut en faire. Une faille de cross-site scripting derrière une Content Security Policy stricte pose un problème bien moindre que la même faille sans elle.
La plupart de ces en-têtes tiennent en une ligne de configuration. L’exception est la CSP, qui demande de la planification. Ce guide explique le rôle de chaque en-tête, des valeurs raisonnables pour une application web typique, et comment les déployer sans casser la production.
Content-Security-Policy
La CSP indique au navigateur quelles sources de scripts, de styles, d’images, de cadres et de connexions sont autorisées. Son rôle principal est d’empêcher l’exécution de scripts injectés. Une liste d’autorisation de domaines semble l’approche évidente, mais elle a un long passif de contournements : tout CDN autorisé qui héberge du contenu utilisateur ou d’anciennes versions de bibliothèques peut servir à charger un script contrôlé par l’attaquant.
Nonces et strict-dynamic
L’approche qui tient la route est une politique basée sur des nonces. Le serveur génère une valeur aléatoire par réponse, la place dans l’en-tête CSP et l’ajoute comme attribut nonce à chaque balise script qu’il rend. Les balises script injectées ne connaissent pas le nonce et sont bloquées. Ajouter 'strict-dynamic' permet aux scripts chargés par un script de confiance d’en charger d’autres, ce qui préserve le fonctionnement des bundlers et des tag managers sans avoir à lister chaque domaine.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-4AEemGb0xJptoIGFP3Nd' 'strict-dynamic';
style-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd';
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsL’en-tête est envoyé sur une seule ligne ; il est présenté ici sur plusieurs lignes pour la lisibilité. Quelques directives de cette politique en font plus qu’il n’y paraît. object-src 'none' bloque les contenus de plugins hérités. base-uri 'none' empêche une balise base injectée de rediriger les URL relatives des scripts. form-action 'self' empêche des formulaires injectés d’envoyer des identifiants ailleurs. Le nonce doit être imprévisible et renouvelé à chaque réponse, ce qui signifie que les pages qui l’utilisent ne peuvent pas être servies depuis un cache statique.
Déployer avec Report-Only
Une CSP stricte déployée à l’aveugle cassera quelque chose : un snippet d’analytics, un gestionnaire d’événement inline, un widget tiers. Publiez d’abord la politique sous forme de Content-Security-Policy-Report-Only. Le navigateur n’applique rien, mais signale chaque violation à l’endpoint indiqué dans la directive report-to ou report-uri. Collectez les rapports pendant un temps, corrigez ou autorisez ce qui est légitime, puis changez le nom de l’en-tête pour passer en mode bloquant. Gardez le reporting actif après l’application, car les nouvelles violations sont soit des régressions, soit des attaques. Attendez-vous à du bruit dans les rapports provenant des extensions de navigateur, qui injectent leurs propres scripts dans les pages ; filtrez-les par source avant de décider quoi autoriser.
Strict-Transport-Security
HSTS indique au navigateur d’utiliser HTTPS pour votre domaine pendant une durée donnée, même si l’utilisateur tape http:// ou clique sur un ancien lien. Cela ferme la fenêtre pendant laquelle un attaquant sur le réseau pourrait intercepter la première requête HTTP en clair et supprimer la redirection vers HTTPS. Les navigateurs ne respectent l’en-tête que s’il arrive via HTTPS, et ils refusent aussi aux utilisateurs de passer outre les erreurs de certificat sur un domaine HSTS : c’est le but recherché, mais aussi le risque si un certificat expire.
Commencez avec un max-age court, par exemple une journée, vérifiez que rien ne casse, puis passez-le à un ou deux ans. includeSubDomains étend la règle à tous les sous-domaines : vérifiez donc d’abord qu’ils servent tous un HTTPS valide, y compris les anciens sites marketing et les outils internes sur le même domaine.
La directive preload, associée à une inscription sur la liste de préchargement des navigateurs, intègre votre domaine dans les navigateurs afin que même la toute première visite utilise HTTPS. Elle exige un max-age d’au moins un an et includeSubDomains. Considérez-la comme une porte à sens unique : le retrait de la liste est possible, mais met longtemps à atteindre les utilisateurs, et tout sous-domaine incapable de servir du HTTPS devient inaccessible entre-temps.
Les en-têtes en une ligne
- X-Content-Type-Options: nosniff. Empêche les navigateurs de deviner un type de contenu différent de celui que vous avez déclaré, ce qui évite qu’un fichier téléversé servi comme texte soit exécuté comme script.
- frame-ancestors (dans la CSP) et X-Frame-Options: DENY. Contrôlent qui peut intégrer vos pages dans un cadre, la défense contre le clickjacking. Les navigateurs modernes utilisent frame-ancestors et ignorent X-Frame-Options lorsque les deux sont définis ; conservez X-Frame-Options pour les clients plus anciens. Utilisez 'self' ou SAMEORIGIN si vous encadrez vos propres pages.
- Referrer-Policy: strict-origin-when-cross-origin. N’envoie aux autres sites que votre origine, pas le chemin complet ni la query string. Cela évite que les jetons et identifiants présents dans les URL ne fuient vers des tiers. Utilisez no-referrer pour les pages particulièrement sensibles.
- Permissions-Policy. Désactive les fonctionnalités du navigateur que vous n’utilisez pas, comme camera=(), microphone=(), geolocation=() et payment=(), afin que du code injecté ou intégré ne puisse pas les demander.
- Cross-Origin-Opener-Policy: same-origin. Place votre page dans son propre groupe de contextes de navigation, afin qu’une fenêtre ouverte par un autre site ne puisse pas conserver de référence vers elle. Utilisez same-origin-allow-popups si vous dépendez de popups OAuth ou de paiement.
- Cross-Origin-Resource-Policy: same-origin ou same-site. Indique aux navigateurs de ne pas laisser d’autres origines charger vos réponses comme images, scripts ou autres sous-ressources. Utilisez same-site si vous servez des ressources depuis un sous-domaine voisin, et cross-origin pour les ressources destinées à être intégrées ailleurs.
Vous pouvez abandonner X-XSS-Protection. Le filtre qu’il contrôlait a été retiré des navigateurs modernes, et la CSP le remplace. Si un scanner insiste, définissez-le à 0. De même, évitez les en-têtes qui ne font que renseigner un attaquant : X-Powered-By et les bannières Server détaillées révèlent gratuitement votre framework et sa version. Dans Next.js, définissez poweredByHeader: false dans la configuration pour supprimer le premier.
Une configuration Next.js
Les en-têtes statiques ont leur place dans next.config.ts, où la fonction headers() les applique à toutes les routes. Les valeurs ci-dessous constituent un point de départ raisonnable pour une application qui ne s’intègre pas elle-même dans des cadres, n’utilise ni la caméra ni la localisation, et sert ses propres ressources. Adaptez chacune d’elles à ce que fait réellement votre application plutôt que de les copier aveuglément.
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=()",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Resource-Policy", value: "same-origin" },
];
const nextConfig: NextConfig = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;La CSP nécessite un nonce neuf à chaque requête : elle est donc définie dans le middleware. Next.js lit le nonce dans l’en-tête Content-Security-Policy de la requête et l’applique aux scripts du framework pendant le rendu. Vous pouvez le lire dans un server component avec headers().get("x-nonce") pour vos propres balises script.
// middleware.ts (in Next.js 16 the file is proxy.ts and the function is proxy)
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' blob: data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};Les pages rendues avec un nonce doivent l’être dynamiquement, puisqu’une page statique réutiliserait le même nonce pour chaque visiteur. Pendant le déploiement, remplacez aux deux endroits le nom de l’en-tête par Content-Security-Policy-Report-Only et ajoutez un endpoint de reporting. En développement, React peut avoir besoin de 'unsafe-eval' dans script-src pour ses fonctionnalités de débogage ; ne l’ajoutez que lorsque NODE_ENV vaut development.
Comment vérifier
- Exécutez curl -sI https://your-domain.example/ sur la page d’accueil, une page dynamique, une route d’API et une ressource statique, et lisez les en-têtes de chacune. Les en-têtes définis uniquement sur les pages HTML sont une lacune fréquente.
- Ouvrez les outils de développement du navigateur. Les violations de CSP apparaissent dans la console avec la directive et la ressource bloquée, et l’onglet Réseau montre les en-têtes réellement servis.
- Vérifiez derrière votre CDN ou votre reverse proxy autant qu’à l’origine. Les proxies suppriment, dupliquent ou écrasent parfois les en-têtes, et deux en-têtes CSP contradictoires sont tous deux appliqués : la politique effective est alors plus stricte que chacune d’elles.
- Utilisez un outil de vérification public comme le Mozilla HTTP Observatory pour un second avis rapide, et le CSP Evaluator de Google pour repérer les directives CSP faibles.
- Ajoutez en CI un test qui interroge les routes clés et vérifie la présence des en-têtes, afin qu’une refonte de la configuration ne puisse pas les supprimer en silence.
Les en-têtes relèvent de la configuration, et c’est précisément pour cela qu’ils dérivent : un nouveau route handler qui construit sa propre réponse, une règle de proxy qui écrase les valeurs par défaut, une CSP assouplie avec 'unsafe-inline' pour débloquer une mise en production. Relisez-les comme du code. CodeAuditAgent lit la configuration d’un dépôt public ou d’un extrait collé en même temps que le reste du code, et signale les en-têtes manquants ou affaiblis avec leur gravité, la configuration citée et un correctif suggéré.
Un ordre de travail raisonnable : les en-têtes en une ligne dès aujourd’hui, HSTS avec un max-age court cette semaine, et une CSP basée sur des nonces en mode Report-Only dès que vous pouvez collecter les rapports.