Dix en-têtes décident de ce que un bug va vous coûter.
Saisissez une URL et nous lisons les en-têtes de réponse que votre serveur envoie réellement. Chacun de ceux qui suivent tient en une ligne de configuration, et chacun fait passer toute une classe d'attaques du possible à l'impossible.
Ce que cela vérifie, et à quoi ressemble une bonne réponse
Content-Security-Policy
Celui qui compte le plus, et celui que presque aucune application générée par IA ne livre. Il indique au navigateur d'où le JavaScript peut venir. Sans lui, chaque endroit où votre application affiche du texte fourni par quelqu'un d'autre, un nom affiché, un commentaire, une description de produit tirée d'une API, devient un endroit où du code peut s'exécuter avec un accès complet à la session. Une bonne réponse part de default-src 'self' et s'élargit délibérément. Une mauvaise est absente, ou si large qu'elle autorise tout ce qu'elle avait été ajoutée pour empêcher.
unsafe-eval, jokers et unsafe-inline dans cette politique
Une politique peut être présente et n'être que décorative. 'unsafe-eval' permet à la page de transformer des chaînes en code exécutable, ce qui est le mécanisme sur lequel repose la plupart des injections. Un * isolé ou un https: dans les sources de scripts signifie que du code peut être chargé depuis n'importe quel hôte d'internet. 'unsafe-inline' signifie que le markup injecté s'exécute. La nuance à connaître : 'strict-dynamic' fait ignorer les sources d'hôte aux navigateurs, et un nonce ou un hash leur fait ignorer 'unsafe-inline' ; une politique qui porte les deux est donc correcte et non contradictoire. Nous appliquons cette règle plutôt que de chercher des motifs dans le texte.
X-Frame-Options et frame-ancestors
Si quelqu'un peut charger votre application dans un frame invisible sur sa propre page, la recouvrir de ses propres boutons et récolter les clics. Cela ne coûte rien à l'attaquant et fonctionne contre toute page où un clic fait quelque chose de sérieux : confirmer un paiement, changer une permission, supprimer un compte. L'un ou l'autre en-tête ferme la porte. frame-ancestors est le moderne et sait nommer les origines autorisées ; X-Frame-Options: DENY est la version brutale que les anciens navigateurs respectent encore.
X-Content-Type-Options
Une seule valeur, nosniff, et elle empêche les navigateurs de deviner le type d'un fichier quand le type déclaré semble faux. C'est par cette devinette qu'un fichier téléversé que votre serveur qualifie de simple document texte finit exécuté comme un script. Si votre application accepte des téléversements et les ressert depuis votre propre domaine, cette ligne n'est pas optionnelle.
Referrer-Policy
Quelle part de l'URL courante voyage avec chaque clic sortant et chaque requête vers un tiers. La valeur par défaut de beaucoup de stacks envoie le chemin complet, ce qui fait fuiter tout ce que vous mettez dans une URL : jetons de réinitialisation de mot de passe, liens d'invitation, identifiants de document, termes de recherche. strict-origin-when-cross-origin garde la partie utile et jette le reste.
Permissions-Policy
Quelles capacités du navigateur la page et ce qu'elle intègre ont le droit de demander : caméra, micro, géolocalisation, paiement. La régler pour refuser ce que vous n'utilisez pas signifie qu'un script tiers compromis ne peut pas en activer une en douce. La plupart des applications n'en ont besoin d'aucune, ce qui en fait l'une des lignes les moins chères du fichier.
Divulgation de la version du serveur
Si vos réponses annoncent la version exacte de ce qui tourne. nginx tout court n'apprend rien à un attaquant qu'il n'aurait pu deviner. nginx/1.18.0 lui dit précisément quels avis de sécurité aller lire. Ce n'est pas une vulnérabilité en soi, c'est l'étape qui précède.
Access-Control-Allow-Origin
Quels autres sites ont le droit de lire vos réponses. Un joker ici, c'est en général quelqu'un qui règle une erreur CORS à trois heures du matin, et cela signifie que n'importe quelle page d'internet peut appeler votre API et lire ce qui revient. Cela vaut d'être revérifié à chaque déploiement, car c'est le réglage le plus susceptible d'avoir été élargi dans l'urgence et jamais resserré.
Comment corriger
Chacun de ces en-têtes se règle à un seul endroit, et sur la plupart des hébergeurs cet endroit est un unique fichier de configuration. La politique ci-dessous est un point de départ assez strict pour valoir la peine et assez souple pour laisser démarrer une application classique : commencez ici, surveillez la console du navigateur, et n'élargissez que là où quelque chose dont vous avez réellement besoin est bloqué.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}# public/_headers
/*
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Content-Security-Policy", value: "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
],
},
];
},
};export default {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
const headers = new Headers(response.headers);
headers.set("content-security-policy", "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'");
headers.set("x-frame-options", "DENY");
headers.set("x-content-type-options", "nosniff");
headers.set("referrer-policy", "strict-origin-when-cross-origin");
headers.set("permissions-policy", "camera=(), microphone=(), geolocation=()");
return new Response(response.body, { status: response.status, headers });
},
};import helmet from "helmet";
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
},
},
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
}),
);Les en-têtes sont le travail de sécurité le moins cher qui soit, et c'est exactement pour cela que les corriger prouve si peu. Un score parfait ici dit que vos réponses sont bien configurées. Il ne dit rien sur le fait que la requête derrière votre tableau de bord vérifie qui pose la question, ni qu'une dépendance installée au printemps dernier a un trou connu, ni qu'une clé d'API traîne dans votre historique git depuis un commit d'il y a six mois. Ces choses-là vivent dans le code, et aucun en-tête ne les compense.