# 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. ```ts // 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. ### buildCsp — generator CSP (zalecane zamiast ręcznego) Zamiast pisać surowy CSP w każdym projekcie (ryzyko pominięcia base-uri, object-src), użyj `buildCsp` — ma twarde reguły OWASP/Lighthouse wbudowane, a Ty włączasz tylko flagi tego, co projekt ładuje: ```ts // next.config.ts import { buildCsp, buildSecurityHeaders } from '@intecion/ipal-kit' const csp = buildCsp({ mode: 'report-only', // zacznij tu; 'enforce' gdy konsola czysta r2Url: process.env.R2_PUBLIC_URL, // media R2 → img-src turnstile: true, // challenges.cloudflare.com → script/frame/connect analytics: true, // GTM + GA youtube: true, // youtube → frame-src googleMaps: true, // mapy Google // extra: { 'script-src': ['https://inny-skrypt.pl'] }, // dodatkowe źródła }) const securityHeaders = buildSecurityHeaders({ hsts: process.env.NODE_ENV === 'production', additional: [csp], }) ``` **Twarde reguły wbudowane** (zawsze, nie da się zapomnieć): `base-uri 'self'`, `object-src 'none'`, `frame-ancestors 'none'`. To te, które Lighthouse/OWASP wymagają, a łatwo je pominąć pisząc CSP ręcznie. `buildCsp` NIE dodaje `'unsafe-eval'` (osłabia CSP) — dodaj przez `extra` tylko jeśli biblioteka tego wymaga. `mode: 'report-only'` daje nagłówek `…-Report-Only`; `'enforce'` daje `Content-Security-Policy`. CSP dalej „w projekcie" (Ty wybierasz flagi wg tego, co ładujesz), ale skeleton jest z pluginu — każdy projekt ma ten sam zahardowany fundament. ### Budowa CSP — ręcznie (jeśli potrzebujesz pełnej kontroli) Domenę mediów czytaj z `R2_PUBLIC_URL` (env), nie zaszywaj. Resztę źródeł dopasuj do tego, co projekt faktycznie ładuje: ```ts // 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: 1. **Najpierw raportowanie** — użyj klucza `Content-Security-Policy-Report-Only` (nie `Content-Security-Policy`). Przeglądarka RAPORTUJE naruszenia w konsoli, ale NIE blokuje — strona działa normalnie. ```ts additional: [{ key: 'Content-Security-Policy-Report-Only', value: csp }] ``` 2. **Otwórz stronę** → DevTools → Console → szukaj „Content Security Policy" violations. Każde naruszenie = brakująca domena. Dodaj ją do odpowiedniej dyrektywy CSP. 3. **Przejdź przez cały serwis** — strona główna, formularze (Turnstile!), galeria (obrazy R2), strony z mapą/embedami. Zbierz wszystkie naruszenia. 4. **Dopiero gdy konsola czysta** → zmień klucz na `Content-Security-Policy` (enforcing). Teraz CSP chroni, nie psując. ### Weryfikacja nagłówków na produkcji ```bash # sprawdź, które nagłówki faktycznie wychodzą: curl -sI https:///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. ## COOP (Cross-Origin-Opener-Policy) — domyślnie włączony buildSecurityHeaders wysyła domyślnie `Cross-Origin-Opener-Policy: same-origin` — izoluje kontekst przeglądarki (ochrona przed XS-Leaks / Spectre, wyciekiem window.opener). Uniwersalny nagłówek, więc z automatu. - Domyślnie `same-origin` (najbezpieczniejsze) - `coop: 'same-origin-allow-popups'` — jeśli otwierasz popupy OAuth/płatności wymagające window.opener - `coop: false` — wyłącz (rzadko potrzebne) ## Trusted Types — NIE wdrażać (na teraz) NIE wymuszaj `require-trusted-types-for 'script'`. Powód: - Audyt Lighthouse to „Bez oceny" (informacyjny/eksperymentalny w Chromium) - Wymuszenie bez kompleksowego silnika polityk w Next/React powoduje `TypeError` przy zewnętrznych skryptach manipulujących DOM stringami (Turnstile, GA) - Zysk bezpieczeństwa nie równoważy ryzyka zepsucia strony Zostaw Trusted Types poza CSP, dopóki Next/React nie da natywnego wsparcia.