Next.js Server Actions automatycznie wystawiają każdą wyeksportowaną funkcję w pliku use-server jako publiczny punkt końcowy HTTP, przypisując jej unikalny identyfikator, którego klient używa do wywołania funkcji. Punkt końcowy istnieje niezależnie od tego, czy na stronie wyrenderowany jest przycisk, link czy jakikolwiek inny element interfejsu użytkownika, co oznacza, że każdy, kto odkryje identyfikator, może wywołać funkcję bezpośrednio.
Projekt frameworka traktuje Server Actions jak zwykłe funkcje pomocnicze, ale w czasie uruchamiania stają się one dostępnymi adresami URL. Dla programistów zakładających, że interfejs użytkownika (UI) jest jedynym strażnikiem, tworzy to niewidoczną powierzchnię ataku, którą można wykorzystać za pomocą pojedynczego, specjalnie przygotowanego żądania.
Dlaczego Server Actions wydawały się bezpieczne
Server Actions wprowadzono, aby umożliwić programistom pisanie kodu po stronie serwera bezpośrednio obok ich komponentów, unikając powtarzalnej struktury (boilerplate) oddzielnych tras API. Typowe użycie wygląda następująco:
<form action={deleteInvoice}>
<button type="submit">Delete</button>
</form>
Ponieważ formularz jest jedynym widocznym sposobem na wywołanie deleteInvoice, wielu programistów ukrywa przycisk przed nieautoryzowanymi użytkownikami, wierzy, że sygnatury TypeScript powstrzymają błędne dane i polega na fakcie, że funkcja znajduje się w module dostępnym tylko dla serwera. Żadne z tych założeń nie zapewnia rzeczywistej ochrony.
Ukryta ekspozycja
Gdy plik zawiera “use server”, Next.js kompiluje każdą wyeksportowaną funkcję do punktu końcowego, takiego jak:
POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>
<action-id> to stabilny hash, który jest osadzany w paczce klienta (client bundle). Atakujący może go uzyskać poprzez:
- Analizę ruchu sieciowego strony.
- Odczytanie spakowanego kodu JavaScript (ID jest zwykłym ciągiem znaków).
- Zgadywanie na podstawie konwencji nazewnictwa, jeśli projekt stosuje przewidywalne wzorce.
Gdy ID zostanie poznane, żądanie można wysłać z dowolnego narzędzia — cURL, Postman lub złośliwego skryptu — omijając wszelkie kontrole na poziomie interfejsu użytkownika.
Trzy konkretne ryzyka
| Ryzyko | Dlaczego to ważne |
|---|---|
| Kontrole UI są nieskuteczne | Ukrycie przycisku lub linku nie usuwa podległego mu punktu końcowego. Punkt końcowy pozostaje dostępny, podobnie jak ukryta strona administracyjna, która wciąż istnieje na serwerze. |
| TypeScript nie zapewnia bezpieczeństwa w czasie wykonywania | Typy są usuwane podczas uruchamiania kodu. Funkcja zadeklarowana jako deleteInvoice(id: number) może otrzymać ogromny ciąg znaków, tablicę lub nawet złośliwy JSON, co prowadzi do błędów logicznych lub ataków typu injection. |
| Brak domyślnego uwierzytelniania lub autoryzacji | Server Actions wyglądają jak lokalne funkcje pomocnicze, więc programiści często zapominają o dodaniu sprawdzania sesji, ochrony CSRF czy kontroli uprawnień na poziomie wiersza, które są standardem w tradycyjnych trasach API. |
Jak zabezpieczyć Server Action
- Uwierzytelnij wywołującego – Sprawdź, czy istnieje ważna sesja lub token, zanim zostanie uruchomiona jakakolwiek logika biznesowa.
- Waliduj ładunek (payload) – Użyj biblioteki schematów (np. Zod, Yup), aby wymusić typy danych i ograniczenia wartości w czasie wykonywania.
- Autoryzuj operację – Poza pytaniem „czy użytkownik jest zalogowany?”, potwierdź, że użytkownik jest właścicielem konkretnego rekordu, który próbuje zmodyfikować lub usunąć.
Minimalny przykład:
'use server';
import { getSession } from '@/auth';
import { z } from 'zod';
import { db } from '@/db';
const DeleteInvoiceSchema = z.object({
id: z.number().int().positive(),
});
export async function deleteInvoice(formData: FormData) {
const session = await getSession();
if (!session) throw new Error('Unauthenticated');
const parsed = DeleteInvoiceSchema.safeParse({
id: Number(formData.get('id')),
});
if (!parsed.success) throw new Error('Invalid input');
const invoice = await db.invoice.findUnique({ where: { id: parsed.data.id } });
if (!invoice || invoice.ownerId !== session.userId) {
throw new Error('Unauthorized');
}
await db.invoice.delete({ where: { id: invoice.id } });
}
Kod jawnie sprawdza uwierzytelnienie, waliduje przychodzące id i upewnia się, że zalogowany użytkownik faktycznie jest właścicielem faktury przed wykonaniem usunięcia.
Inne subtelne pułapki Next.js
| Problem | Objaw | Rozwiązanie |
|---|---|---|
Zmienne środowiskowe NEXT_PUBLIC_ |
Wszystko, co ma prefiks NEXT_PUBLIC_, jest dołączane do paczki klienta, co ujawnia sekrety. |
Przechowuj sekrety w zwykłych zmiennych środowiskowych, nigdy nie dodawaj do nich prefiksu NEXT_PUBLIC_. |
| Otwarte przekierowania | Przyjęcie parametru zapytania redirect i naiwne jego łączenie może przekierować użytkowników na //evil.com. |
Waliduj cel względem białej listy lub wymuszaj sprawdzanie tej samej domeny (same-origin). |
| Server-Side Request Forgery (SSRF) | Pobieranie adresu URL dostarczonego przez użytkownika może pozwolić atakującym na dotarcie do wewnętrznych usług lub punktów końcowych metadanych chmury. | Stosuj białą listę nazw hostów, blokuj prywatne zakresy adresów IP i ustaw limity czasu (timeouts). |
dangerouslySetInnerHTML |
Renderowanie dostarczonego przez użytkownika kodu HTML bez sanityzacji otwiera drogę do ataków XSS. | Użyj biblioteki takiej jak DOMPurify lub całkowicie unikaj surowego kodu HTML. |
Automatyczne skanowanie tych wzorców
Narzędzia do analizy statycznej mogą oznaczać ryzykowne konstrukcje wymienione powyżej. Jedną z lekkich opcji jest Semgrep, który działa szybko i może zostać zintegrowany z potokami CI:
npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .
Zestaw reguł obejmuje sprawdzanie wyeksportowanych funkcji serwerowych, niewłaściwego użycia NEXT_PUBLIC_, otwartych przekierowań, wzorców SSRF oraz niebezpiecznego wstawiania kodu HTML.
Co warto śledzić dalej
- Aktualizacje frameworka – Śledź wydania Next.js; zespół może wprowadzić wbudowane hooki uwierzytelniające lub izolować (sandbox) generowane punkty końcowe.
- Narzędzia społeczności – Pojawiają się nowe wtyczki ESLint oraz reguły Semgrep specyficzne dla Next.js, które mają na celu automatyzację zabezpieczeń opisanych w tym miejscu.
- Incydenty w świecie rzeczywistym – W miarę jak coraz więcej projektów wdraża Server Actions, zwracaj uwagę na ujawnione exploity, które ilustrują to ryzyko w praktyce. Wczesne wykrycie może pomóc w przeprowadzeniu wewnętrznych przeglądów bezpieczeństwa, zanim dojdzie do naruszenia.
Wniosek: Next.js Server Action nie jest prywatną funkcją pomocniczą; staje się publicznym punktem końcowym HTTP w momencie jego eksportu. Traktuj go jak każdą inną trasę API—uwierzytelniaj, waliduj i autoryzuj—w przeciwnym razie wygoda pisania kodu serwerowego obok UI może szybko stać się zagrożeniem dla bezpieczeństwa.
