5.6 KiB
security
Generyczne nagłówki bezpieczeństwa HTTP (HSTS, X-Frame-Options, nosniff,
Referrer-Policy, Permissions-Policy) jako funkcja do next.config. CSP CELOWO
pominięte — zależy od domen projektu, zostaje w projekcie.
Zasada
Nagłówki, które są IDENTYCZNE między projektami, plugin dostarcza raz. CSP
(Content-Security-Policy) wymaga znajomości domen konkretnego projektu (skąd
ładują się skrypty, obrazy, fonty, analytics), więc nie może być generyczne —
zostaje w projekcie, dodawane przez additional.
Użycie — next.config.ts
Nagłówki wpina się w next.config, NIE w proxy — bo muszą pokryć CAŁĄ
aplikację (też /admin, statyki), a proxy pomija te trasy.
// next.config.ts
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
import { buildSecurityHeaders } from '@intecion/ipal-kit'
const securityHeaders = buildSecurityHeaders({
hsts: process.env.NODE_ENV === 'production', // WAŻNE: off w dev (http)
additional: [
// CSP projektu — zna swoje domeny:
// { key: 'Content-Security-Policy', value: "default-src 'self'; ..." },
],
})
const nextConfig: NextConfig = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }]
},
}
export default withPayload(nextConfig)
Opcje
| Opcja | Domyślnie | Rola |
|---|---|---|
hsts |
true |
Strict-Transport-Security (wymuś HTTPS) |
hstsMaxAge |
63072000 (2 lata) |
max-age HSTS w sekundach |
hstsIncludeSubDomains |
true |
HSTS na subdomeny |
hstsPreload |
false |
preload (tylko jeśli zgłaszasz do listy) |
frameOptions |
'DENY' |
X-Frame-Options (anty-clickjacking) |
referrerPolicy |
'strict-origin-when-cross-origin' |
Referrer-Policy |
permissionsPolicy |
blokuje camera/mic/geolocation | Permissions-Policy |
additional |
[] |
dodatkowe nagłówki (np. CSP); same-key nadpisuje |
PUŁAPKA — HSTS w dev
HSTS nad HTTP na localhost może zablokować przeglądarkę na HTTPS dla localhost.
ZAWSZE wyłączaj w dev: hsts: process.env.NODE_ENV === 'production'.
Nadpisywanie i CSP
additional z tym samym kluczem NADPISUJE domyślny (np. zmień X-Frame-Options
na SAMEORIGIN). Nowy klucz (jak CSP) dodaje. CSP zawsze przez additional —
plugin go nie generuje, bo zależy od projektu.
Dlaczego CSP zostaje w projekcie (nie plugin)
HSTS, nosniff, Referrer-Policy są IDENTYCZNE dla każdego projektu → plugin je
generuje. CSP wylicza KONKRETNE domeny, z których projekt ładuje (jego R2,
analytics, Turnstile, fonty). Generyczny CSP byłby albo za luźny (* =
bezużyteczny), albo psułby stronę. Więc plugin daje mechanizm (additional),
projekt dostarcza CSP dopasowany do siebie.
Budowa CSP — domeny z env, nie hardkod
Domenę mediów czytaj z R2_PUBLIC_URL (env), nie zaszywaj. Resztę źródeł
dopasuj do tego, co projekt faktycznie ładuje:
// next.config.ts
const r2Url = process.env.R2_PUBLIC_URL || ''
const csp = [
"default-src 'self'",
// skrypty: self + Turnstile (Cloudflare) + analytics (GTM/GA jeśli używasz)
"script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://www.googletagmanager.com",
// style: self + inline (Tailwind) + Google Fonts
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
// obrazy: self + media R2 (z env!) + data:
`img-src 'self' data: ${r2Url}`.trim(),
"font-src 'self' https://fonts.gstatic.com data:",
"connect-src 'self' https://www.google-analytics.com",
// ramki: Turnstile (widget captcha)
"frame-src https://challenges.cloudflare.com",
"form-action 'self'",
"frame-ancestors 'none'", // zastępuje X-Frame-Options w nowych przeglądarkach
].join('; ')
const securityHeaders = buildSecurityHeaders({
hsts: process.env.NODE_ENV === 'production',
additional: [{ key: 'Content-Security-Policy', value: csp }],
})
Dopasuj źródła do projektu: mapy Google (https://maps.googleapis.com,
https://*.google.com), inne embedy, inne analytics. To, czego nie wymienisz,
zostanie zablokowane.
WDRAŻAJ CSP OSTROŻNIE — najpierw Report-Only
CSP za ścisły psuje stronę (blokuje skrypty/style/obrazy). NIGDY nie wdrażaj enforcing CSP na ślepo. Metoda bezpieczna:
- Najpierw raportowanie — użyj klucza
Content-Security-Policy-Report-Only(nieContent-Security-Policy). Przeglądarka RAPORTUJE naruszenia w konsoli, ale NIE blokuje — strona działa normalnie.additional: [{ key: 'Content-Security-Policy-Report-Only', value: csp }] - Otwórz stronę → DevTools → Console → szukaj „Content Security Policy" violations. Każde naruszenie = brakująca domena. Dodaj ją do odpowiedniej dyrektywy CSP.
- Przejdź przez cały serwis — strona główna, formularze (Turnstile!), galeria (obrazy R2), strony z mapą/embedami. Zbierz wszystkie naruszenia.
- Dopiero gdy konsola czysta → zmień klucz na
Content-Security-Policy(enforcing). Teraz CSP chroni, nie psując.
Weryfikacja nagłówków na produkcji
# sprawdź, które nagłówki faktycznie wychodzą:
curl -sI https://<DOMENA>/pl | grep -i "strict-transport\|content-type-options\|referrer\|content-security\|x-frame"
Jeśli HSTS/nosniff/Referrer są, a CSP brak → dodaj CSP (wyżej). Jeśli BRAK
wszystkich mimo buildSecurityHeaders w config → sprawdź, czy headers() jest
wpięte i czy Cloudflare (jeśli przed aplikacją) nie filtruje nagłówków.
Uwaga Cloudflare: jeśli CF jest przed aplikacją, może nadpisywać/filtrować nagłówki. Wtedy ustaw je też w CF (Transform Rules → Modify Response Header) albo upewnij się, że CF przepuszcza nagłówki z origin.