Przejdź do treści

CSRF, XSS, Injection: 5 luk bezpieczeństwa w Next.js

Myślisz, że Twój kod w Next.js jest bezpieczny? Zobacz, jak nieświadomie otwierasz aplikację na ataki XSS, CSRF i injection oraz jak szybko to naprawić.

Maciej Sala

Founder StriveLab

9 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 12 lipca 2026)

Server Actions, Route Handlers, Server Components i proxy poszerzają powierzchnię ataku. Przejdźmy po kolei przez miejsca, które najczęściej pękają w prawdziwych aplikacjach.

1. XSS w Next.js, czyli Cross-Site Scripting

Gdzie pęka ochrona przed XSS?

to atak, w którym napastnik wstrzykuje złośliwy skrypt wykonywany w przeglądarce ofiary. React domyślnie escapuje zawartość JSX, więc {userInput} jest bezpieczne. Ale są dwa klasyczne sposoby na obejście tej ochrony.

dangerouslySetInnerHTML — najczęstszy wektor

Code
// PODATNE — HTML z CMS renderowany bez sanityzacji
export default function BlogPost({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      {/* Payload może zawierać np. <img src=x onerror="..."> */}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

URL-e z danych użytkownika

React 19 zastępuje URL-e javascript: w atrybutach funkcją zgłaszającą błąd, więc poniższy przykład nie wykona już payloadu w aktualnym stosie Next.js. Nie traktuj tego jednak jako ogólnego walidatora URL: starszy React, surowe API DOM, biblioteki zewnętrzne, przekierowania i inne miejsca użycia adresu mogą nie mieć tej ochrony.

Code
// NIEPOPRAWNE — komponent bez własnej polityki dozwolonych URL-i
<a href={userProvidedUrl}>Profil użytkownika</a>

Jak zamknąć lukę XSS w Next.js?

Code
npm install isomorphic-dompurify
Code
// BEZPIECZNE — sanityzacja HTML przed renderowaniem
import DOMPurify from 'isomorphic-dompurify'
 
export default function BlogPost({ post }: { post: Post }) {
  const sanitizedContent = DOMPurify.sanitize(post.content, {
    ALLOWED_TAGS: [
      'h1',
      'h2',
      'h3',
      'p',
      'a',
      'ul',
      'ol',
      'li',
      'strong',
      'em',
      'code',
      'pre',
      'img',
      'blockquote',
    ],
    ALLOWED_ATTR: ['href', 'src', 'alt', 'class'],
  })
 
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
    </article>
  )
}
Code
// BEZPIECZNE — walidacja URL
function SafeLink({
  url,
  children,
}: {
  url: string
  children: React.ReactNode
}) {
  let safeUrl: URL
 
  try {
    safeUrl = new URL(url)
  } catch {
    return <span>{children}</span>
  }
 
  if (!['https:', 'http:'].includes(safeUrl.protocol)) {
    return <span>{children}</span> // Renderuj jako tekst, nie link
  }
 
  return (
    <a href={safeUrl.href} rel="noopener noreferrer">
      {children}
    </a>
  )
}

Open redirect — ten sam problem, inny wektor

Bliski krewny podatnego linku to open redirect: przekierowanie na adres pochodzący wprost od użytkownika. Najczęściej pojawia się przy powrotach po logowaniu (?next=...), gdzie atakujący podstawia obcą domenę i wysyła ofierze wiarygodnie wyglądający link do Twojej strony, który ląduje na phishingu.

Code
// PODATNE — redirect na dowolny adres z query string
import { redirect } from 'next/navigation'
 
export async function login(formData: FormData) {
  // ...uwierzytelnienie...
  const next = formData.get('next') as string
  redirect(next) // next = "https://phishing.example" — Next.js przekieruje
}
Code
// BEZPIECZNE — po normalizacji dopuść wyłącznie własny origin
const next = (formData.get('next') as string) || '/dashboard'
const appOrigin = 'https://example.com'
let safeNext = '/dashboard'
 
try {
  const target = new URL(next, appOrigin)
  if (target.origin === appOrigin) {
    safeNext = `${target.pathname}${target.search}${target.hash}`
  }
} catch {}
 
redirect(safeNext)

Parsowanie przez URL jest pewniejsze niż samo startsWith('/'): normalizuje adres i pozwala odrzucić zarówno //evil.example, jak i mniej oczywiste warianty z ukośnikami wstecznymi. W produkcji trzymaj kanoniczny origin w zwalidowanej konfiguracji, nie wpisuj go w wielu plikach.

2. Wyciek danych z serwera do klienta w Next.js

Problem bezpieczeństwa

Server Components mogą pobierać wrażliwe dane (hasła, tokeny, pełne rekordy DB), ale jeśli przekażesz je do Client Componenta, React musi je zserializować w payloadzie RSC wysyłanym do przeglądarki. Nie oznacza to, że rekord staje się częścią statycznego pliku JavaScript, ale użytkownik nadal może odczytać przesłane dane w odpowiedzi sieciowej.

Code
// PODATNE — pełny obiekt user (z hashem hasła!) przekazany do klienta
export default async function ProfilePage() {
  const user = await db.user.findUnique({
    where: { id: session.userId },
  })
 
  // user zawiera passwordHash, role, internalNotes...
  return <ProfileClient user={user} />
}

Rozwiązanie w Next.js

Przekazuj do Client Components tylko pola, które klient potrzebuje:

Code
// BEZPIECZNE — selektywny przekaz danych
export default async function ProfilePage() {
  const user = await db.user.findUnique({
    where: { id: session.userId },
    select: {
      id: true,
      name: true,
      email: true,
      avatar: true,
      // NIE: passwordHash, role, internalNotes
    },
  })
 
  return <ProfileClient user={user} />
}

Dodatkowe zabezpieczenie — :

Code
// lib/dto.ts
interface PublicUser {
  id: string
  name: string
  email: string
  avatar: string | null
}
 
export function toPublicUser(user: DbUser): PublicUser {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    avatar: user.avatar,
  }
}

React 19 wprowadza experimental_taintObjectReference i experimental_taintUniqueValue — mechanizmy, które wyrzucą błąd, jeśli spróbujesz przekazać wrażliwy obiekt do Client Component. Nazwa nie jest przypadkowa, ponieważ to wciąż API eksperymentalne i domyślnie wyłączone. Żeby zadziałało, włącz je w konfiguracji:

Code
// next.config.ts
const nextConfig = {
  experimental: {
    taint: true,
  },
}

Traktuj je więc jako dodatkową siatkę bezpieczeństwa, a nie jako podstawową linię obrony, ponieważ tą jest select i DTO powyżej.

3. Brak walidacji inputu w Server Actions

Problem walidacji danych

Server Actions tworzą publicznie osiągalne endpointy POST. Next.js utrudnia wywołanie ich z obcego originu, ale zalogowany użytkownik nadal może sam skonstruować żądanie i przesłać pola, których formularz nie pokazuje. Bez walidacji prowadzi to do mass assignment: nie wstrzykujesz kodu SQL, tylko przemycasz do zapytania pole, które nigdy nie powinno pochodzić od użytkownika.

Code
// PODATNE — brak walidacji
'use server'
 
export async function updateProfile(formData: FormData) {
  const name = formData.get('name') as string
  const role = formData.get('role') as string // Użytkownik może zmienić sobie rolę!
 
  await db.user.update({
    where: { id: session.userId },
    data: { name, role }, // atakujący ustawia role: 'admin' zwykłym POST-em
  })
}

Klasyczny SQL injection jest mniej prawdopodobny, gdy konsekwentnie używasz parametryzowanych metod ORM. Nie znaczy to jednak, że ORM automatycznie zabezpiecza każde API: surowe zapytania (db.$queryRawUnsafe, sklejane stringi SQL), dynamiczne fragmenty ORDER BY i filtry budowane bez allowlisty przywracają injection natychmiast. Parametryzuj wartości, a nazwy kolumn i dozwolone operatory mapuj z zamkniętej listy. Ta sama zasada dotyczy wywołań powłoki, nie sklejaj inputu użytkownika w komendę dla exec tylko użyj API bez shella i przekaż argumenty osobno.

Rozwiązanie przez walidację runtime

Code
// BEZPIECZNE — walidacja z Zod + kontrola pól
'use server'
 
import { z } from 'zod'
import { auth } from '@/auth'
 
const updateProfileSchema = z.object({
  name: z.string().min(2).max(100).trim(),
  bio: z.string().max(500).optional(),
  // NIE akceptuj 'role', 'email', 'id' z formularza
})
 
export async function updateProfile(formData: FormData) {
  const session = await auth()
  if (!session) throw new Error('Unauthorized')
 
  const parsed = updateProfileSchema.safeParse({
    name: formData.get('name'),
    bio: formData.get('bio'),
  })
 
  if (!parsed.success) {
    return { error: 'Nieprawidłowe dane', details: parsed.error.flatten() }
  }
 
  // Aktualizuj TYLKO zwalidowane pola
  await db.user.update({
    where: { id: session.user.id },
    data: parsed.data,
  })
 
  revalidatePath('/profile')
  return { success: true }
}

Server Action to publiczny endpoint POST. Nigdy nie przekazuj surowego formData do bazy i zawsze waliduj oraz ograniczaj pola.

zasada Server Actions

CSRF w Route Handlerach, o którym Next.js Cię nie ostrzeże

Tu pojawia się luka, którą łatwo przeoczyć, bo dotyczy tylko części Twojego kodu. Server Actions mają wbudowaną ochronę: Next.js sprawdza, czy Origin żądania zgadza się z Host (lub X-Forwarded-Host), i wykonuje akcję tylko przez POST. To realne zabezpieczenie — ale działa wyłącznie dla Server Actions.

Własne Route Handlery (app/api/.../route.ts) mutujące dane na podstawie cookie sesyjnego tej ochrony nie dostają. Jeśli logujesz użytkownika przez cookie, taki endpoint jest podatny na klasyczny atak: obca strona wysyła do niego żądanie w tle, przeglądarka dołącza cookie automatycznie, a serwer wykonuje mutację, myśląc, że to autoryzowany użytkownik.

Code
// PODATNE — Route Handler mutujący na podstawie cookie, bez sprawdzania Origin
// app/api/account/delete/route.ts
export async function POST() {
  const session = await auth() // czyta cookie sesyjne
  if (!session) return new Response('Unauthorized', { status: 401 })
 
  await db.user.delete({ where: { id: session.user.id } })
  // Obca strona wywoła to POST-em, a przeglądarka dołączy cookie ofiary.
  return Response.json({ ok: true })
}

Najprostsza obrona to powtórzenie tego, co Server Actions robią automatycznie — odrzucenie żądań, których Origin nie zgadza się z hostem:

Code
// BEZPIECZNE — weryfikacja Origin dla mutacji opartych o cookie
export async function POST(request: Request) {
  const origin = request.headers.get('origin')
  // X-Forwarded-Host czytaj tylko za zaufanym reverse proxy.
  const host =
    request.headers.get('x-forwarded-host') ?? request.headers.get('host')
 
  let validOrigin = false
  try {
    validOrigin = Boolean(origin && host && new URL(origin).host === host)
  } catch {}
 
  if (!validOrigin) {
    return new Response('Bad origin', { status: 403 })
  }
 
  const session = await auth()
  if (!session) return new Response('Unauthorized', { status: 401 })
 
  await db.user.delete({ where: { id: session.user.id } })
  return Response.json({ ok: true })
}

Sprawdzanie Origin jest dobrą warstwą obrony, ale musi uwzględniać topologię proxy i sposób obsługi żądań bez tego nagłówka. W wypadku operacji wysokiego ryzyka użyj sprawdzonego tokenu CSRF (synchronizer token albo poprawnie podpisany double-submit cookie), ustaw sesyjne cookies jako Secure, HttpOnly i co najmniej SameSite=Lax, a pomocniczo możesz filtrować Sec-Fetch-Site. Samo SameSite jest obroną warstwową, a nie uniwersalnym zamiennikiem tokenu. Przeniesienie mutacji do Server Actions daje wbudowaną kontrolę originu, lecz nie zwalnia z autoryzacji i walidacji. Więcej o samym uwierzytelnianiu, sesjach i tokenach znajdziesz w artykule o backendzie dla frontendowca: auth, real-time i integracje.

4. Brak nagłówków bezpieczeństwa w Next.js

Problem z security headers

Next.js nie zna modelu zagrożeń Twojej aplikacji, dlatego nie ustawi za Ciebie kompletnej polityki nagłówków. Bez niej przeglądarka nie dostaje m.in. jasnych reguł osadzania strony, ładowania skryptów i ujawniania referrera.

Rozwiązanie przez nagłówki bezpieczeństwa

Code
// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff', // Blokuj MIME sniffing
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=(self)',
          },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains',
          },
        ],
      },
    ]
  },
}

HSTS wysyłaj wyłącznie po HTTPS. includeSubDomains dodaj dopiero wtedy, gdy każda subdomena działa po HTTPS; opcja preload wymaga jeszcze ostrożniejszej, świadomej decyzji. Clickjacking najlepiej blokować dyrektywą CSP frame-ancestors 'none'; X-Frame-Options: DENY możesz zachować jako warstwę zgodności dla starszych klientów.

Content Security Policy, czyli CSP w Next.js

to najpotężniejszy nagłówek bezpieczeństwa, który definiuje, z jakich źródeł przeglądarka może ładować skrypty, style, obrazy i inne zasoby. Tyle że to też nagłówek, który najłatwiej wdrożyć źle, więc przejdź go w całości, a nie połowicznie.

Choć to potężny nagłówek bezpieczeństwa kontrolujący źródła skryptów, stylów i innych zasobów, jest on również wyjątkowo podatny na błędy konfiguracyjne. Połowiczne wdrożenie CSP daje złudne poczucie bezpieczeństwa lub, co gorsza, potrafi całkowicie zablokować prawidłowe działanie aplikacji.

Jak to działa? Proxy generuje losowy nonce na każde żądanie, dopisuje go do dyrektywy script-src i przekazuje dalej, a Next.js oznacza tym nonce swoje własne skrypty. Kluczowe jest to drugie: bez przekazania nonce do aplikacji dyrektywa 'strict-dynamic' zablokuje skrypty samego Next.js i wyświetli białą stronę. Dlatego nonce wędruje w nagłówku żądania (x-nonce), skąd odczyta go layout:

Code
// proxy.ts
import { NextResponse, type NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  const nonce = btoa(crypto.randomUUID())
 
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data: https:;
    font-src 'self' https://fonts.gstatic.com;
    connect-src 'self' https://api.example.com;
    frame-ancestors 'none';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
  `
    .replace(/\s{2,}/g, ' ')
    .trim()
 
  // Nonce w nagłówku ŻĄDANIA — stąd odczyta go layout i oznaczy skrypty.
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  // Next.js odczytuje nonce właśnie z CSP obecnego w nagłówku żądania.
  requestHeaders.set('Content-Security-Policy', cspHeader)
 
  const response = NextResponse.next({ request: { headers: requestHeaders } })
  response.headers.set('Content-Security-Policy', cspHeader)
 
  return response
}
Code
// app/layout.tsx — nonce trzeba przekazać ręcznie np. do zewnętrznego Script
import { headers } from 'next/headers'
import Script from 'next/script'
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const nonce = (await headers()).get('x-nonce') ?? undefined
 
  return (
    <html lang="pl">
      <body>
        {children}
        <Script src="https://example.com/analytics.js" nonce={nonce} />
      </body>
    </html>
  )
}

5. Ekspozycja zmiennych środowiskowych w Next.js

Problem z publicznymi env vars

W Next.js odwołania do zmiennych z prefiksem NEXT_PUBLIC_ są zastępowane ich wartościami podczas next build i mogą trafić do kodu klienta. Zmienne bez tego prefiksu pozostają serwerowe, ale tylko dopóki sam nie przekażesz ich do Client Componenta, odpowiedzi API albo logów.

Code
# .env.local
DATABASE_URL=postgresql://user:password@host/db    # PRYWATNE — OK
STRIPE_SECRET_KEY=sk_live_xxx                      # PRYWATNE — OK
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx      # PUBLICZNE Z ZAŁOŻENIA — OK
NEXT_PUBLIC_DATABASE_URL=postgresql://...           # KATASTROFA!

Rozwiązanie dla sekretów i zmiennych środowiskowych

Code
// lib/env.ts — walidacja zmiennych przy starcie
import 'server-only'
import { z } from 'zod'
 
const serverEnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  REVALIDATION_SECRET: z.string().min(32),
  AUTH_SECRET: z.string().min(32),
})
 
// Walidacja server env
export const serverEnv = serverEnvSchema.parse({
  DATABASE_URL: process.env.DATABASE_URL,
  STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
  REVALIDATION_SECRET: process.env.REVALIDATION_SECRET,
  AUTH_SECRET: process.env.AUTH_SECRET,
})

Moduł z server-only nie może jednocześnie eksportować konfiguracji przeznaczonej dla Client Components. W praktyce rozdziel env.server.ts (sekrety i import 'server-only') od env.client.ts (wyłącznie jawna allowlista NEXT_PUBLIC_*). W tym drugim odwołuj się do zmiennych statycznie, np. process.env.NEXT_PUBLIC_APP_URL, bo dynamiczny dostęp przez process.env[name] nie zostanie wstawiony do bundle'a. Walidację uruchamiaj w procesie build/start; pamiętaj też, że publiczne wartości są zamrażane podczas buildu, więc ten sam artefakt promowany między środowiskami zachowa wartości z chwili kompilacji.

Sekret bez prefiksu też może wyciec

Brak NEXT_PUBLIC_ nie jest mechanizmem kontroli dostępu. Taki kod nadal ujawnia sekret:

Code
// PODATNE — odpowiedź API jawnie wysyła sekret
export async function GET() {
  return Response.json({ token: process.env.INTERNAL_API_TOKEN })
}

Oprócz walidacji pilnuj granic serwer–klient, maskuj sekrety w logach i po wykryciu wycieku natychmiast rotuj dane dostępowe. Usunięcie wartości z repozytorium lub bundle'a nie unieważnia już ujawnionego klucza.

Bonus: rate limiting endpointów auth

Pięć luk powyżej zamyka najczęstsze dziury, ale jedna rzecz przewija się przez wszystkie i zasługuje na osobne zdanie: brak limitowania żądań. Walidacja i autoryzacja chronią przed pojedynczym złym żądaniem, ale nie przed tysiącem poprawnych. Endpoint logowania bez limitu to zaproszenie do brute force, a Server Action wysyłający e-maile bez limitu może zamienić Twoją domenę w narzędzie do spamu.

Minimalna ochrona to licznik na parę „akcja + IP (lub userId)", oparty o współdzielony store (Upstash Redis na edge, bo pamięć procesu nie przetrwa między wywołaniami serverless). Nakładaj go przede wszystkim na logowanie, rejestrację, reset hasła i każdą operację wysyłającą wiadomości. To część minimum bezpieczeństwa każdej publicznej aplikacji.

Bonus: SSRF przy pobieraniu URL-i na serwerze

Gdy Route Handler pobiera adres podany przez użytkownika, przykładowo do generowania podglądu linku, importu obrazu albo webhooka testowego, atakujący może skierować serwer na localhost, prywatny adres sieciowy lub endpoint metadanych chmury. To : żądanie wychodzi z zaufanej infrastruktury, więc omija bariery niedostępne z internetu.

Nie wystarczy sprawdzić, czy tekst zaczyna się od https://, więc używaj allowlisty hostów, po rozwiązaniu DNS odrzucaj adresy prywatne, link-local i loopback, wyłącz lub kontroluj przekierowania oraz ustaw limit czasu i rozmiaru odpowiedzi. Walidację powtarzaj po każdym redirectcie, bo bez tego bezpieczny URL może przekierować na zasób wewnętrzny. Jeśli funkcja biznesowa nie wymaga dowolnych hostów, nie dawaj jej takiej możliwości.

Lista kontrolna bezpieczeństwa Next.js: CSRF, XSS, injection i env vars

Przed wdrożeniem na produkcję sprawdź:

  1. Każdy dangerouslySetInnerHTML ma sanityzację (DOMPurify),
  2. URL-e od użytkownika (linki, redirecty ?next=) są walidowane — bez javascript: i obcych domen,
  3. Route Handlery mutujące na podstawie cookie sprawdzają Origin/Host (CSRF)
  4. Cookies sesyjne mają Secure, HttpOnly i odpowiednie SameSite; operacje wysokiego ryzyka używają tokenu CSRF,
  5. Każdy Server Action waliduje input (Zod) i sprawdza autoryzację,
  6. Żadne pole trafiające do bazy nie pochodzi wprost z formData (mass assignment),
  7. Surowe SQL, nazwy kolumn i komendy systemowe nie są sklejane z inputem użytkownika,
  8. Client Components nie dostają wrażliwych danych (passwordHash, tokeny)
  9. Nagłówki bezpieczeństwa są skonfigurowane (CSP z frame-ancestors, HSTS, nosniff),
  10. Zmienne środowiskowe prywatne nie mają prefiksu NEXT_PUBLIC_,
  11. Rate limiting chroni endpointy auth i API,
  12. Serwer nie pobiera dowolnych URL-i; ochrona SSRF obejmuje DNS i przekierowania,
  13. Linki otwierane przez target="_blank" używają noopener tam, gdzie wspierasz starsze przeglądarki; noreferrer dodajesz tylko wtedy, gdy chcesz ukryć referrer.
Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Next.js

Często zadawane pytania

Czy React i Next.js chronią mnie przed XSS automatycznie?

Tylko częściowo, ponieważ React escapuje zawartość JSX, więc {userInput} jest bezpieczne w kontekście tekstowym. Ale dangerouslySetInnerHTML renderuje surowy HTML; nawet jeśli wstawiony tą drogą element <script> zwykle się nie wykona, XSS nadal umożliwiają np. atrybuty zdarzeń i niebezpieczne URL-e. React 19 blokuje javascript: w atrybutach URL, ale aplikacja nadal musi walidować adresy używane poza JSX i w przekierowaniach. Sanityzuj HTML przez DOMPurify i waliduj URL-e przed użyciem.

Czy Next.js ma wbudowaną ochronę CSRF?

Server Actions mają zabezpieczenie same-origin, w którym Next.js porównuje Origin z Host lub X-Forwarded-Host, a akcje działają tylko przez POST. Route Handlers nie dostają tego automatycznie, bo dla mutacji opartych o cookies dodaj sprawdzanie Origin/Host albo klasyczne tokeny CSRF.

Jak zabezpieczyć Server Actions przed nadużyciem?

Traktuj je jak publiczne endpointy API. Next.js odrzuca żądania z niedozwolonego Origin, ale nadal zawsze sprawdzaj autoryzację (auth()), waliduj input przez Zod i ograniczaj akceptowane pola. Nigdy nie przekazuj surowego formData do bazy, ponieważ atakujący może podstawić pola w rodzaju role czy id.

Jak uniknąć wycieku wrażliwych danych do klienta?

Server Component pobierający pełny rekord (z passwordHash, role, internalNotes) i przekazujący go do Client Componenta serializuje te dane w strumieniu React Server Components wysyłanym do przeglądarki. Używaj select w zapytaniu, mapuj na DTO z minimalnym zestawem pól, a w React 19 wspomóż się experimental_taintObjectReference, by wymusić błąd przy próbie przekazania wrażliwego obiektu.

Jakie nagłówki bezpieczeństwa skonfigurować w Next.js?

Next.js nie ustawia za aplikację kompletnej polityki. Minimum to X-Content-Type-Options: nosniff (MIME sniffing), Referrer-Policy, Permissions-Policy i Strict-Transport-Security. Najpotężniejszy jest Content Security Policy z nonce, ustawiany w proxy — definiuje, z jakich źródeł przeglądarka może ładować skrypty i style, a frame-ancestors blokuje clickjacking.

Jak przetestować bezpieczeństwo aplikacji Next.js?

Łącz testy autoryzacji i walidacji w kodzie z analizą zależności, SAST oraz skanem DAST, np. OWASP ZAP na środowisku testowym. Osobno testuj, czy użytkownik A nie może zmienić zasobów użytkownika B, czy obcy Origin jest odrzucany i czy CSP działa w trybie Report-Only bez nieoczekiwanych naruszeń. Żaden pojedynczy skaner nie zastępuje tych testów.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Route Handlers czy Server Actions? Kiedy co wybrać

App Router daje Ci dwa sposoby na uruchomienie kodu po stronie serwera, poprzez Route Handlers i Server Actions . Może wyglądają podobnie, ponieważ oba działają na serwerze, sięgają do bazy i do zmiennych środowiskowych, ale każdy z nich rozwiązuje zupełnie inny problem. Użycie jednego tam, gdzie pasuje drugi, nie jest odpowiednim rozwiązaniem, ponieważ zły wybór odbija się potem na architekturze.

Maciej Sala

Maciej Sala

Founder StriveLab

Bezpieczna autentykacja w React: Gdzie trzymać token?

Nie ma prostej odpowiedzi na to, gdzie trzymać token w aplikacji React: to ciągły kompromis między XSS a CSRF. LocalStorage i sessionStorage są podatne na kradzież przez XSS, a z kolei ciasteczka z flagą httpOnly skutecznie blokują dostęp skryptom JS, ale wystawiają aplikację na ataki CSRF. Trzymanie tokenu w pamięci RAM chroni przed jednym i drugim, ale znika przy każdym odświeżeniu strony...

Maciej Sala

Maciej Sala

Founder StriveLab

Middleware w Next.js — 7 zastosowań i typowych pułapek

Większość deweloperów może kojarzyć middleware w Next.js ze sprawdzaniem, czy użytkownik jest zalogowany, podczas gdy dla żądań objętych matcherem może ono podjąć decyzję jeszcze przed routingiem: przypisać wariant eksperymentu, zastosować limit, wykonać rewrite albo dodać zależny od requestu nagłówek. W artykule chciałem omówić siedem optymalnych i bezpiecznych zastosowań Proxy.

Maciej Sala

Maciej Sala

Founder StriveLab