CSRF w Next.js – jak skutecznie chronić aplikację przy użyciu SameSite i double‑submit token

Praktyczny przewodnik po ochronie aplikacji Next.js przed atakami CSRF – SameSite cookies i metoda double‑submit token w jednym rozwiązaniu.

CSRF w Next.js – jak skutecznie chronić aplikację przy użyciu SameSite i double‑submit token

Cross‑Site Request Forgery (CSRF) pozostaje jednym z najczęściej wykorzystywanych wektorów ataku w aplikacjach webowych. Choć frameworki takie jak Next.js oferują wiele mechanizmów ułatwiających budowę bezpiecznych interfejsów, programiści często pomijają podstawowe środki ochrony. W tym artykule pokażemy, krok po kroku, jak zabezpieczyć aplikację Next.js przed CSRF, łącząc ustawienia SameSite cookies oraz wzorzec double‑submit token. Omówimy model zagrożenia, mechanikę działania, przykłady kodu, checklistę mitygacji oraz typowe pułapki.

Model zagrożenia – jak wygląda atak CSRF

Atak CSRF polega na wykorzystaniu autoryzowanego kontekstu przeglądarki ofiary. Użytkownik jest zalogowany w aplikacji, a następnie odwiedza złośliwą stronę, która wysyła żądanie HTTP do chronionego endpointu (np. POST /api/transfer). Przeglądarka automatycznie dołącza ciasteczka sesyjne, dzięki czemu serwer uważa żądanie za autentyczne. Bez dodatkowego mechanizmu weryfikacji serwer nie odróżnia takiego żądania od zamierzonego działania użytkownika.

Dlaczego SameSite cookies nie wystarczą samodzielnie

Ustawienie flagi SameSite=Lax lub SameSite=Strict w ciasteczku sesyjnym ogranicza jego automatyczne przesyłanie w kontekście cross‑site. Jednak w praktyce istnieją scenariusze, w których przeglądarki zezwalają na przesyłanie ciasteczka (np. żądania GET w trybie Lax lub nieobsługiwane starsze przeglądarki). Dlatego rekomenduje się łączenie SameSite z dodatkowym tokenem, który jest weryfikowany po stronie serwera.

Double‑submit token – zasada działania

Wzorzec double‑submit token polega na wygenerowaniu losowego tokenu po stronie serwera i przesłaniu go zarówno w ciasteczku, jak i w nagłówku (lub ciele) żądania. Serwer przy odbiorze żądania porównuje dwie wartości – jeśli się zgadzają, żądanie jest uznane za autentyczne. Token nie wymaga przechowywania po stronie serwera (stateless), co upraszcza skalowanie.

„Podwójny token to najprostszy sposób na wprowadzenie silnej weryfikacji CSRF bez wprowadzania sesji po stronie serwera.”

Implementacja w Next.js – część serwerowa

Next.js udostępnia API Routes, które działają jako funkcje serwerowe. Poniżej znajdziesz fragment kodu, który generuje token, zapisuje go w ciasteczku SameSite=Lax oraz weryfikuje przy kolejnych żądaniach.

import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

const CSRF_COOKIE = 'csrfToken';
const CSRF_HEADER = 'x-csrf-token';

function generateToken() {
  return crypto.randomBytes(32).toString('hex');
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // 1️⃣ Generowanie tokenu przy GET (np. przy renderowaniu formularza)
  if (req.method === 'GET') {
    const token = generateToken();
    res.setHeader('Set-Cookie', `${CSRF_COOKIE}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure`);
    return res.status(200).json({ csrfToken: token });
  }

  // 2️⃣ Weryfikacja przy POST/PUT/DELETE
  const cookieToken = req.cookies[CSRF_COOKIE];
  const headerToken = req.headers[CSRF_HEADER];

  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  // … dalsza logika endpointu
  return res.status(200).json({ success: true });
}

W powyższym przykładzie token jest generowany przy pierwszym GET i zwracany zarówno w ciasteczku, jak i w odpowiedzi JSON. Klient (np. komponent React) powinien odczytać token i umieścić go w nagłówku x-csrf-token przy kolejnych żądaniach mutujących.

Implementacja w Next.js – część kliencka (React)

W komponentach React możemy użyć hooka useEffect do pobrania tokenu i ustawienia domyślnego nagłówka w bibliotece axios lub fetch. Poniżej przykład z fetch:

import { useEffect } from 'react';

export default function useCsrf() {
  useEffect(() => {
    fetch('/api/csrf')
      .then(r => r.json())
      .then(data => {
        // token dostępny w data.csrfToken oraz w cookie
        window.csrfToken = data.csrfToken;
      });
  }, []);
}

export async function csrfFetch(url, options = {}) {
  const headers = {
    ...options.headers,
    'x-csrf-token': window.csrfToken,
  };
  return fetch(url, { ...options, headers, credentials: 'include' });
}

Ustawienie credentials: 'include' zapewnia, że ciasteczko z tokenem zostanie dołączone do żądania, a jednocześnie nagłówek x-csrf-token umożliwia weryfikację podwójną.

Checklisty mitygacji CSRF w Next.js

  • Ustaw flagę SameSite=Lax (lub Strict jeśli aplikacja nie wymaga cross‑site POST) oraz Secure w każdym ciasteczku sesyjnym.
  • Generuj unikalny token per sesję i przechowuj go w http‑only cookie.
  • Wysyłaj token w dedykowanym nagłówku (x-csrf-token) przy wszystkich żądaniach mutujących.
  • Weryfikuj zgodność tokenu cookie i nagłówka po stronie serwera przed wykonaniem logiki biznesowej.
  • Ogranicz czas życia tokenu (np. 30 min) i odświeżaj go przy każdej sesji.
  • Upewnij się, że wszystkie endpointy API przyjmujące dane (POST, PUT, DELETE, PATCH) mają włączoną weryfikację CSRF.

Typowe błędy i kompromisy

Programiści często popełniają następujące błędy:

  • Brak flagi SameSite – w starszych przeglądarkach token może zostać wykorzystany, co osłabia ochronę.
  • Użycie ciasteczka nie‑httpOnly – umożliwia odczyt tokenu przez złośliwy skrypt, co eliminuje korzyść z podwójnego tokenu.
  • Przechowywanie tokenu w lokalnym storage – naraża go na kradzież w wyniku XSS.
  • Sprawdzanie tokenu tylko po stronie klienta – nie zapewnia rzeczywistej ochrony, bo atakujący może pominąć kod JavaScript.
  • Ustawienie SameSite=Strict w aplikacji, która wymaga zewnętrznych odnośników – może spowodować nieoczekiwane odrzucanie legalnych żądań.

W praktyce najbezpieczniej jest połączyć SameSite z double‑submit token, a jednocześnie stosować zasady least‑privilege – minimalizować uprawnienia ciasteczek i tokenów, ograniczając ich zasięg do niezbędnych ścieżek (Path=/api).

Odwołania do standardów i najlepszych praktyk

Opisane techniki są zgodne z wytycznymi OWASP Top 10 – A05:2021 – Security Misconfiguration oraz CWE‑352 (Cross‑Site Request Forgery). Dodatkowo, stosowanie flagi SameSite oraz zasady least‑privilege jest rekomendowane w OWASP Secure Headers Project. Implementacja double‑submit token spełnia wymóg OWASP ASVS 2.2.1 (CSRF Prevention).

Praktyczny przykład – pełny kod API i hook React

Poniżej kompletny, gotowy do skopiowania przykład, który można umieścić w /pages/api/csrf.ts oraz w własnym hooku /hooks/useCsrf.ts. Dzięki temu masz jednocześnie endpoint generujący token i prostą funkcję do wykonywania zabezpieczonych żądań.

// /pages/api/csrf.ts
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

const COOKIE_NAME = 'csrfToken';
const HEADER_NAME = 'x-csrf-token';

function newToken() {
  return crypto.randomBytes(24).toString('base64');
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    const token = newToken();
    res.setHeader('Set-Cookie', `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Secure; Path=/; Max-Age=1800`);
    return res.status(200).json({ csrfToken: token });
  }

  const cookieToken = req.cookies[COOKIE_NAME];
  const headerToken = req.headers[HEADER_NAME];
  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  // tutaj logika chronionego endpointu
  return res.status(200).json({ message: 'Protected action succeeded' });
}

// /hooks/useCsrf.ts
import { useEffect } from 'react';

export function useCsrf() {
  useEffect(() => {
    fetch('/api/csrf', { credentials: 'include' })
      .then(r => r.json())
      .then(data => {
        (window as any).csrfToken = data.csrfToken;
      });
  }, []);
}

export async function csrfFetch(input: RequestInfo, init: RequestInit = {}) {
  const token = (window as any).csrfToken;
  const headers = new Headers(init.headers);
  headers.set('x-csrf-token', token);
  return fetch(input, { ...init, headers, credentials: 'include' });
}

Po zaimportowaniu useCsrf w komponencie, token zostanie pobrany przy montażu, a każde wywołanie csrfFetch automatycznie dołączy wymagany nagłówek.

Podsumowanie i dalsze kroki

Ochrona przed CSRF w aplikacjach Next.js nie wymaga skomplikowanych rozwiązań – wystarczy połączyć SameSite cookies, flagi Secure oraz wzorzec double‑submit token. Działając zgodnie z OWASP, CWE i zasadą least‑privilege, zapewniasz solidną barierę przed nieautoryzowanymi żądaniami. Jeśli potrzebujesz audytu bezpieczeństwa, pomocy przy integracji lub chcesz podnieść poziom ochrony całej platformy, skontaktuj się z Coderia.it – razem zbudujemy aplikację odporną na najnowsze zagrożenia.

Zacznijmy

Masz projekt na oku?

Opisz go w kilku zdaniach — odpiszę w ciągu 24 godzin z bezpłatną wyceną i propozycją stacku.