Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Re-check authorization inside Next.js server actions and RSC data loads
Server actions and server-component data fetches are directly invokable endpoints. Perform the session and object-level authorization check inside them, not solely in middleware or a parent layout.
Bad example
| 1 | // app/dashboard/layout.tsx — assumes this guards everything below |
| 2 | export default async function Layout({ children }) { |
| 3 | const session = await getSession(); |
| 4 | if (!session) redirect('/login'); |
| 5 | return <>{children}</>; |
| 6 | } |
| 7 |
|
| 8 | // app/dashboard/actions.ts — relies on the layout having run |
| 9 | 'use server'; |
| 10 | export async function updatePortfolioName(id: number, name: string) { |
| 11 | // No auth here. Server actions are POST endpoints callable directly; |
| 12 | // the layout does NOT run before an action invocation. |
| 13 | await db.portfolio.update({ where: { id }, data: { name } }); |
| 14 | } |
Explanation (EN)
Layouts and middleware do not reliably gate server actions — an action is its own POST endpoint that an attacker can invoke directly with a crafted request, bypassing any UI navigation through the layout. With no session or ownership check inside the action, anyone can rename any portfolio by id. The same applies to data fetched in server components.
Objašnjenje (HR)
Layouti i middleware ne čuvaju pouzdano server actione — action je vlastiti POST endpoint koji napadač može izravno pozvati posebno oblikovanim zahtjevom, zaobilazeći svaku UI navigaciju kroz layout. Bez provjere sesije ili vlasništva unutar actiona, bilo tko može preimenovati bilo koji portfelj po id-u. Isto vrijedi za podatke dohvaćene u server komponentama.
Good example
| 1 | // app/dashboard/actions.ts |
| 2 | 'use server'; |
| 3 | export async function updatePortfolioName(id: number, name: string) { |
| 4 | const session = await getSession(); |
| 5 | if (!session) throw new Error('Unauthorized'); |
| 6 |
|
| 7 | const parsedName = z.string().min(1).max(120).parse(name); |
| 8 |
|
| 9 | // Owner-scoped update: a non-owner matches 0 rows. |
| 10 | const result = await db.portfolio.updateMany({ |
| 11 | where: { id, ownerId: session.userId }, |
| 12 | data: { name: parsedName }, |
| 13 | }); |
| 14 | if (result.count === 0) throw new Error('Not found'); |
| 15 | } |
Explanation (EN)
The action establishes the session and scopes the mutation by owner inside itself, so it is safe regardless of how it was invoked or whether any layout/middleware ran. Treat every server action and server-component data load as an independent, directly-reachable endpoint that must authorize itself.
Objašnjenje (HR)
Action uspostavlja sesiju i ograničava mutaciju po vlasniku unutar sebe, pa je siguran bez obzira na to kako je pozvan ili je li ijedan layout/middleware izvršen. Tretiraj svaki server action i dohvat podataka u server komponenti kao neovisan, izravno dostupan endpoint koji se mora sam autorizirati.
Exceptions / Tradeoffs (EN)
Next.js middleware is fine for coarse, defense-in-depth gating (redirect unauthenticated users, attach headers) but must never be the only authorization layer; matcher gaps and the fact that middleware can be bypassed for certain invocation paths make it insufficient on its own. Keep the authoritative check next to the data access.
Iznimke / Tradeoffi (HR)
Next.js middleware je u redu za grubo, dubinsko ograničavanje (preusmjeri neautentificirane korisnike, dodaj headere) ali nikad ne smije biti jedini sloj autorizacije; rupe u matcheru i činjenica da se middleware može zaobići za određene putanje poziva čine ga nedostatnim samim po sebi. Mjerodavnu provjeru drži uz pristup podacima.