Zehn Header entscheiden, wie teuer ein Bug für dich wird.
Gib eine URL ein und wir lesen die Response-Header, die dein Server tatsächlich schickt. Jeder davon ist eine einzige Zeile Konfiguration, und jeder macht aus einer ganzen Angriffsklasse etwas Unmögliches.
Was hier geprüft wird, und wie eine gute Antwort aussieht
Content-Security-Policy
Der wichtigste, und der, den fast keine KI-generierte App ausliefert. Er sagt dem Browser, woher JavaScript kommen darf. Ohne ihn wird jede Stelle, an der deine App Text von jemand anderem darstellt, ein Anzeigename, ein Kommentar, eine über eine API geholte Produktbeschreibung, zu einer Stelle, an der Code mit vollem Zugriff auf die Session laufen kann. Eine gute Antwort beginnt bei default-src 'self' und öffnet sich bewusst. Eine schlechte fehlt ganz oder ist so weit, dass sie alles erlaubt, was sie verhindern sollte.
unsafe-eval, Wildcards und unsafe-inline in dieser Policy
Eine Policy kann vorhanden und trotzdem Dekoration sein. 'unsafe-eval' erlaubt der Seite, Strings in laufenden Code zu verwandeln, und darauf setzen die meisten Injections auf. Ein einzelnes * oder ein https: in den Skriptquellen bedeutet, dass Code von jedem Host im Internet geladen werden darf. 'unsafe-inline' bedeutet, dass injiziertes Markup ausgeführt wird. Die Feinheit, die man kennen sollte: 'strict-dynamic' lässt Browser Host-Quellen ignorieren, und eine Nonce oder ein Hash lässt sie 'unsafe-inline' ignorieren. Eine Policy mit beidem ist also korrekt und nicht widersprüchlich. Wir wenden diese Regel an, statt im Text nach Mustern zu suchen.
X-Frame-Options und frame-ancestors
Ob jemand deine App in einem unsichtbaren Frame auf der eigenen Seite laden, mit eigenen Buttons überdecken und die Klicks einsammeln kann. Den Angreifer kostet das nichts, und es funktioniert gegen jede Seite, auf der ein Klick etwas Ernstes tut: eine Zahlung bestätigen, eine Berechtigung ändern, ein Konto löschen. Beide Header schließen das. frame-ancestors ist der moderne und kann erlaubte Origins benennen; X-Frame-Options: DENY ist die grobe Variante, die alte Browser weiterhin respektieren.
X-Content-Type-Options
Ein einziger Wert, nosniff, und er hindert Browser daran, den Typ einer Datei zu raten, wenn der deklarierte Typ falsch aussieht. Genau dieses Raten führt dazu, dass eine hochgeladene Datei, die dein Server als reines Textdokument ausweist, als Skript ausgeführt wird. Wenn deine App Uploads annimmt und sie von deiner eigenen Domain wieder ausliefert, ist diese Zeile nicht optional.
Referrer-Policy
Wie viel der aktuellen URL bei jedem ausgehenden Klick und jeder Drittanfrage mitreist. Die Voreinstellung vieler Stacks schickt den vollen Pfad mit, was alles preisgibt, was du in eine URL packst: Passwort-Reset-Tokens, Einladungslinks, Dokument-IDs, Suchbegriffe. strict-origin-when-cross-origin behält den nützlichen Teil und wirft den Rest weg.
Permissions-Policy
Welche Browser-Fähigkeiten die Seite und alles, was sie einbettet, anfordern dürfen: Kamera, Mikrofon, Standort, Zahlung. Alles zu verbieten, was du nicht nutzt, heißt, dass ein kompromittiertes Fremdskript nichts davon still einschalten kann. Die meisten Apps brauchen keine davon, was das zu einer der billigsten Zeilen in der Datei macht.
Preisgabe der Serverversion
Ob deine Antworten die genaue Version dessen nennen, was da läuft. nginx allein verrät einem Angreifer nichts, was er nicht raten könnte. nginx/1.18.0 sagt ihm exakt, welche Sicherheitsmeldungen er nachlesen soll. Das ist für sich keine Schwachstelle, es ist der Schritt davor.
Access-Control-Allow-Origin
Welche anderen Seiten deine Antworten lesen dürfen. Eine Wildcard hier ist meistens jemand, der um drei Uhr nachts einen CORS-Fehler wegräumt, und sie bedeutet, dass jede Seite im Internet deine API aufrufen und lesen kann, was zurückkommt. Das lohnt sich bei jedem Deploy nachzusehen, denn diese Einstellung wird am ehesten in Eile geweitet und nie wieder verengt.
So behebst du es
Jeder dieser Header wird an einer einzigen Stelle gesetzt, und bei den meisten Hostern ist diese Stelle eine einzige Konfigurationsdatei. Die Policy unten ist ein Startpunkt, streng genug, um etwas zu taugen, und locker genug, damit eine typische App hochkommt: fang hier an, beobachte die Browser-Konsole und öffne nur dort, wo wirklich etwas blockiert wird, das du brauchst.
{
"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" },
}),
);Header sind die billigste Sicherheitsarbeit, die es gibt, und genau deshalb beweist ihre Behebung so wenig. Ein perfekter Wert hier sagt, dass deine Antworten gut konfiguriert sind. Er sagt nichts darüber, ob die Abfrage hinter deinem Dashboard prüft, wer da fragt, ob eine im vergangenen Frühjahr installierte Dependency ein bekanntes Loch hat, oder ob ein API-Key aus einem Commit von vor sechs Monaten in deiner Git-Historie liegt. Das lebt im Code, und kein Header gleicht es aus.