JavaScript / TypeScript/05advanced12 min

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 Probe na 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: dangerouslySetInnerHTML wymaga 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.

RyzykoObrona
Nieprawidłowe dane z APIwalidacja zod na granicy
XSSbrak dangerouslySetInnerHTML lub DOMPurify
Wyciek sekretubrak NEXT_PUBLIC_, fetch z serwera
Otwarte przekierowaniewhitelist 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.local jest 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.