Nagłówki bezpieczeństwa bez tajemnic: CSP, HSTS i reszta
Praktyczny przewodnik po CSP z nonce i strict-dynamic, HSTS i preload, frame-ancestors, Referrer-Policy, Permissions-Policy i COOP, z konfiguracją Next.js.
· 7 min czytania · Lina Source LLC
Nagłówki bezpieczeństwa to instrukcje, które Twój serwer przekazuje przeglądarce: uruchamiaj tylko te skrypty, rozmawiaj ze mną wyłącznie przez HTTPS, nie pozwól innym witrynom osadzać tej strony w ramce. Nie naprawiają błędów w Twoim kodzie, ale ograniczają to, co atakujący może z nimi zrobić. Dziura typu cross-site scripting za ścisłą Content Security Policy to znacznie mniejszy problem niż ta sama dziura bez niej.
Większość z tych nagłówków to jedna linia konfiguracji. Wyjątkiem jest CSP, które wymaga planowania. Ten przewodnik opisuje, co robi każdy nagłówek, jakie wartości mają sens w typowej aplikacji webowej i jak je wdrożyć, nie psując produkcji.
Content-Security-Policy
CSP mówi przeglądarce, które źródła skryptów, stylów, obrazów, ramek i połączeń są dozwolone. Jego głównym zadaniem jest powstrzymanie wstrzykniętych skryptów przed uruchomieniem. Lista dozwolonych domen brzmi jak oczywiste podejście, ale ma długą historię obejść: dowolny dozwolony CDN, który hostuje treści użytkowników albo stare wersje bibliotek, można wykorzystać do wczytania skryptu kontrolowanego przez atakującego.
Nonce i strict-dynamic
Podejściem, które się broni, jest polityka oparta na nonce. Serwer generuje losową wartość dla każdej odpowiedzi, umieszcza ją w nagłówku CSP i dodaje jako atrybut nonce do każdego renderowanego znacznika script. Wstrzyknięte znaczniki script nie znają tego nonce i są blokowane. Dodanie 'strict-dynamic' pozwala skryptom wczytanym przez zaufany skrypt wczytywać kolejne skrypty, dzięki czemu bundlery i menedżery tagów działają bez wymieniania każdej domeny.
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-requestsNagłówek wysyłany jest w jednej linii; tutaj został zawinięty dla czytelności. Kilka dyrektyw w tej polityce robi więcej, niż się wydaje. object-src 'none' blokuje treści starych wtyczek. base-uri 'none' powstrzymuje wstrzyknięty znacznik base przed przekierowaniem względnych adresów skryptów. form-action 'self' nie pozwala wstrzykniętym formularzom wysyłać poświadczeń gdzie indziej. Nonce musi być nieprzewidywalne i nowe w każdej odpowiedzi, co oznacza, że strony z niego korzystające nie mogą być serwowane ze statycznej pamięci podręcznej.
Wdrażaj w trybie Report-Only
Ścisła CSP wdrożona na ślepo coś zepsuje: fragment kodu analitycznego, wbudowany handler zdarzeń, widżet innej firmy. Wypuść politykę najpierw jako Content-Security-Policy-Report-Only. Przeglądarka niczego nie egzekwuje, ale raportuje każde naruszenie do endpointu wskazanego w dyrektywie report-to lub report-uri. Zbieraj raporty przez pewien czas, napraw lub dopuść to, co jest uzasadnione, a potem zmień nazwę nagłówka na egzekwującą. Utrzymaj raportowanie także po włączeniu egzekwowania, bo nowe naruszenia to albo regresje, albo ataki. Spodziewaj się szumu w raportach od rozszerzeń przeglądarki, które wstrzykują na strony własne skrypty; odfiltruj je według źródła, zanim zdecydujesz, co dopuścić.
Strict-Transport-Security
HSTS mówi przeglądarce, żeby przez określony czas używała HTTPS dla Twojej domeny, nawet jeśli użytkownik wpisze http:// albo kliknie stary link. Zamyka to okno, w którym atakujący w sieci mógłby przechwycić pierwsze zwykłe żądanie HTTP i usunąć przekierowanie na HTTPS. Przeglądarki honorują ten nagłówek tylko wtedy, gdy dotrze przez HTTPS, i nie pozwalają użytkownikom przeklikać się przez błędy certyfikatu na domenie z HSTS, co jest sednem tego mechanizmu, ale też ryzykiem, jeśli certyfikat wygaśnie.
Zacznij od krótkiego max-age, na przykład jednego dnia, potwierdź, że nic się nie psuje, a potem podnieś go do roku lub dwóch. includeSubDomains rozszerza regułę na każdą subdomenę, więc najpierw sprawdź, czy wszystkie serwują prawidłowe HTTPS, w tym stare strony marketingowe i narzędzia wewnętrzne w tej samej domenie.
Dyrektywa preload w połączeniu ze zgłoszeniem na listę preload przeglądarek wpisuje Twoją domenę na stałe w przeglądarki, dzięki czemu nawet pierwsza wizyta odbywa się przez HTTPS. Wymaga max-age wynoszącego co najmniej rok i includeSubDomains. Traktuj to jak drzwi w jedną stronę: usunięcie z listy jest możliwe, ale bardzo długo dociera do użytkowników, a każda subdomena, która nie potrafi obsłużyć HTTPS, staje się w międzyczasie nieosiągalna.
Nagłówki na jedną linię
- X-Content-Type-Options: nosniff. Powstrzymuje przeglądarki przed zgadywaniem typu treści innego niż zadeklarowany, co zapobiega wykonaniu jako skrypt przesłanego pliku serwowanego jako tekst.
- frame-ancestors (w CSP) i X-Frame-Options: DENY. Decydują, kto może osadzać Twoje strony w ramce, co jest obroną przed clickjackingiem. Nowoczesne przeglądarki używają frame-ancestors i ignorują X-Frame-Options, gdy ustawione są oba; zachowaj X-Frame-Options dla starszych klientów. Użyj 'self' lub SAMEORIGIN, jeśli osadzasz własne strony.
- Referrer-Policy: strict-origin-when-cross-origin. Wysyła do innych witryn wyłącznie Twoje pochodzenie, a nie pełną ścieżkę i parametry zapytania. Dzięki temu tokeny i identyfikatory w adresach URL nie wyciekają do podmiotów trzecich. Dla szczególnie wrażliwych stron użyj no-referrer.
- Permissions-Policy. Wyłącza funkcje przeglądarki, z których nie korzystasz, takie jak camera=(), microphone=(), geolocation=() czy payment=(), żeby wstrzyknięty lub osadzony kod nie mógł o nie prosić.
- Cross-Origin-Opener-Policy: same-origin. Umieszcza Twoją stronę we własnej grupie kontekstów przeglądania, więc okno otwarte przez inną witrynę nie może zachować referencji do niej. Użyj same-origin-allow-popups, jeśli polegasz na wyskakujących okienkach OAuth lub płatności.
- Cross-Origin-Resource-Policy: same-origin lub same-site. Mówi przeglądarkom, żeby nie pozwalały innym źródłom wczytywać Twoich odpowiedzi jako obrazów, skryptów czy innych podzasobów. Użyj same-site, jeśli serwujesz zasoby z siostrzanej subdomeny, i cross-origin dla zasobów przeznaczonych do osadzania gdzie indziej.
X-XSS-Protection możesz pominąć. Filtr, którym sterował, został usunięty z nowoczesnych przeglądarek, a jego zamiennikiem jest CSP. Jeśli jakiś skaner się upiera, ustaw go na 0. Podobnie unikaj nagłówków, które dodają jedynie informacji atakującemu: X-Powered-By i szczegółowe banery Server za darmo zdradzają Twój framework i jego wersję. W Next.js ustaw poweredByHeader: false w konfiguracji, żeby usunąć ten pierwszy.
Konfiguracja Next.js
Statyczne nagłówki należą do next.config.ts, gdzie funkcja headers() stosuje je do każdej trasy. Poniższe wartości to rozsądny domyślny zestaw dla aplikacji, która nie osadza się w ramkach, nie korzysta z kamery ani lokalizacji i serwuje własne zasoby. Dostosuj każdą z nich do tego, co Twoja aplikacja faktycznie robi, zamiast kopiować je na ślepo.
// 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;CSP potrzebuje świeżego nonce dla każdego żądania, więc ustawia się je zamiast tego w middleware. Next.js odczytuje nonce z nagłówka Content-Security-Policy żądania i stosuje je do własnych skryptów frameworka podczas renderowania. We własnych znacznikach script możesz je odczytać w komponencie serwerowym przez headers().get("x-nonce").
// middleware.ts (w Next.js 16 plik nazywa się proxy.ts, a funkcja 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).*)"],
};Strony renderowane z nonce muszą być renderowane dynamicznie, bo strona statyczna użyłaby jednego nonce dla wszystkich odwiedzających. Podczas wdrażania zmień nazwę nagłówka w obu miejscach na Content-Security-Policy-Report-Only i dodaj endpoint raportujący. W trybie deweloperskim React może potrzebować 'unsafe-eval' w script-src do swoich funkcji debugowania; dodaj je tylko wtedy, gdy NODE_ENV ma wartość development.
Jak to zweryfikować
- Uruchom curl -sI https://twoja-domena.example/ dla strony głównej, strony dynamicznej, trasy API i zasobu statycznego i przeczytaj nagłówki każdego z nich. Nagłówki ustawione tylko na stronach HTML to częsta luka.
- Otwórz narzędzia deweloperskie przeglądarki. Naruszenia CSP pojawiają się w konsoli wraz z dyrektywą i zablokowanym zasobem, a karta Network pokazuje nagłówki, które faktycznie zostały wysłane.
- Sprawdź także za swoim CDN-em lub odwrotnym proxy, nie tylko na serwerze źródłowym. Proxy czasem usuwają, duplikują lub nadpisują nagłówki, a dwa sprzeczne nagłówki CSP są egzekwowane oba, więc efektywna polityka jest ostrzejsza niż każdy z nich.
- Skorzystaj z publicznego narzędzia sprawdzającego, takiego jak Mozilla HTTP Observatory, dla szybkiej drugiej opinii, oraz z CSP Evaluator od Google, żeby wychwycić słabe dyrektywy CSP.
- Dodaj do CI test, który odpytuje kluczowe trasy i sprawdza obecność nagłówków, żeby refaktoryzacja konfiguracji nie mogła ich po cichu usunąć.
Nagłówki to konfiguracja i właśnie dlatego się rozjeżdżają: nowy route handler budujący własną odpowiedź, reguła proxy nadpisująca ustawienia domyślne, CSP rozluźnione przez 'unsafe-inline', żeby odblokować wydanie. Przeglądaj je jak kod. CodeAuditAgent czyta konfigurację w publicznym repozytorium lub wklejonym fragmencie kodu razem z resztą kodu i zgłasza brakujące lub osłabione nagłówki wraz z wagą, zacytowaną konfiguracją i proponowaną poprawką.
Rozsądna kolejność prac: nagłówki na jedną linię dzisiaj, HSTS z krótkim max-age w tym tygodniu, a CSP oparte na nonce w trybie Report-Only, gdy tylko będziesz w stanie zbierać raporty.