Bezpieczne wzorce (walidacja, XSS, sekrety)
Frontend ZEUS jest częścią produktu bezpieczeństwa — sam musi być bezpieczny. Trzy najczęstsze pułapki to brak walidacji danych z zewnątrz, XSS i sekrety wyciekające do bundla. Tę lekcję poświęcamy właśnie im.
Waliduj wszystko, co przychodzi z zewnątrz
Odpowiedź API, parametry URL, localStorage — to dane, którym nie ufamy.
Walidujemy je zod-em i dopiero wtedy używamy.
import { z } from "zod";
const ProbeSchema = z.object({
id: z.string(),
host: z.string(),
forest: z.string(),
});
export async function getProbe(id: string) {
const res = await fetch(`/api/probes/${id}`);
const raw: unknown = await res.json();
return ProbeSchema.parse(raw); // rzuci, jeśli kształt się nie zgadza
}
Standard ProfessNet: granica z sieci/storage'u zawsze przechodzi przez schemat zod. Nie rzutujemy
as Probena surowy JSON — to kłamstwo wobec kompilatora.
XSS — nie wstrzykuj surowego HTML
React domyślnie escapuje treść, więc {userInput} jest bezpieczne. Niebezpieczne
jest dangerouslySetInnerHTML.
// bezpiecznie — React escapuje
<span>{probe.host}</span>
// niebezpiecznie — XSS, jeśli treść pochodzi od użytkownika
<div dangerouslySetInnerHTML={{ __html: probe.note }} />
Jeśli naprawdę musisz renderować HTML (np. opis z markdown), najpierw go sanityzuj:
import DOMPurify from "isomorphic-dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
Standard ProfessNet:
dangerouslySetInnerHTMLwymaga sanityzacji (DOMPurify) i komentarza uzasadniającego w PR. Bez tego review odrzuca zmianę.
Sekrety — NEXT_PUBLIC_ to świadoma decyzja
W Next.js zmienne z prefiksem NEXT_PUBLIC_ trafiają do bundla i są widoczne
w przeglądarce. Wszystko inne zostaje na serwerze.
// PUBLICZNE — widoczne dla każdego w przeglądarce
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// SEKRET — tylko po stronie serwera (Server Component / Route Handler)
const apiKey = process.env.ZEUS_API_KEY; // bez NEXT_PUBLIC_
// app/api/scan/route.ts — sekret używany tylko na serwerze
export async function POST(req: Request) {
const res = await fetch("https://backend/scan", {
headers: { authorization: `Bearer ${process.env.ZEUS_API_KEY}` },
});
return Response.json(await res.json());
}
Standard ProfessNet: klucz API ani token nigdy nie mają prefiksu
NEXT_PUBLIC_. Wywołania wymagające sekretu idą przez Route Handler / Server Action, nie z przeglądarki.
| Ryzyko | Obrona |
|---|---|
| Nieprawidłowe dane z API | walidacja zod na granicy |
| XSS | brak dangerouslySetInnerHTML lub DOMPurify |
| Wyciek sekretu | brak NEXT_PUBLIC_, fetch z serwera |
| Otwarte przekierowanie | whitelist dozwolonych URL-i |
Nagłówki bezpieczeństwa
W next.config.js ustawiamy CSP i podstawowe nagłówki:
headers: async () => [
{
source: "/(.*)",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
],
},
];
Wskazówka:
.env.localjest w.gitignore. W repo trzymamy tylko.env.example. Sekret w historii git = rotacja klucza.
Trzy nawyki: waliduj wejście, nie wstrzykuj surowego HTML, trzymaj sekrety na serwerze. To 90% bezpieczeństwa frontu, a kosztuje kilka linii kodu więcej.