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-validate on the server; client-side validation is never a security boundary
Always re-run validation server-side (in the API route / server action) even when the frontend validates — client checks are bypassable and exist only for UX.
Bad example
| 1 | 'use server'; |
| 2 | // Server Action assumes the client form already validated with Zod. |
| 3 | export async function createPost(data: { title: string; authorId: string; published: boolean }) { |
| 4 | // No server-side validation: a crafted fetch can send any title length, |
| 5 | // any authorId (impersonation), or flip published on a draft. |
| 6 | return prisma.post.create({ data }); |
| 7 | } |
Explanation (EN)
Server Actions and API routes are public HTTP endpoints; the React form's Zod check never runs for an attacker who calls them directly. Trusting client validation here allows impersonation via a forged `authorId`, oversized payloads, and bypassed business rules — the server is the only place validation actually constrains anything.
Objašnjenje (HR)
Server Actions i API rute su javni HTTP endpointi; Zod provjera React forme nikad se ne izvrši za napadača koji ih zove izravno. Vjerovanje klijentskoj validaciji ovdje omogućuje impersonaciju preko krivotvorenog `authorId`, prevelike payloade i zaobiđena poslovna pravila — server je jedino mjesto gdje validacija stvarno nešto ograničava.
Good example
| 1 | // shared/schemas.ts — one schema imported by both the form and the server |
| 2 | import { z } from 'zod'; |
| 3 | export const CreatePostSchema = z.object({ |
| 4 | title: z.string().min(1).max(200), |
| 5 | published: z.boolean(), |
| 6 | }).strict(); |
| 7 |
|
| 8 | // app/actions.ts |
| 9 | 'use server'; |
| 10 | import { CreatePostSchema } from '@/shared/schemas'; |
| 11 | import { getSessionUser } from '@/lib/auth'; |
| 12 |
|
| 13 | export async function createPost(input: unknown) { |
| 14 | const parsed = CreatePostSchema.safeParse(input); |
| 15 | if (!parsed.success) return { error: parsed.error.flatten() }; |
| 16 | const user = await getSessionUser(); // authorId comes from the session, never the client |
| 17 | return prisma.post.create({ data: { ...parsed.data, authorId: user.id } }); |
| 18 | } |
Explanation (EN)
The same schema validates on the server, so the action is safe no matter how it's called. Identity-bearing fields like `authorId` are derived from the authenticated session, not the request body, closing the impersonation gap. Sharing the schema between client and server keeps UX validation and the security boundary in lockstep.
Objašnjenje (HR)
Ista shema validira na serveru, pa je akcija sigurna bez obzira na to kako je pozvana. Polja koja nose identitet poput `authorId` izvode se iz autenticirane sesije, a ne iz tijela zahtjeva, čime se zatvara prostor za impersonaciju. Dijeljenje sheme između klijenta i servera drži UX validaciju i sigurnosnu granicu usklađenima.
Notes (EN)
Maps to OWASP A04:2021 Insecure Design and ASVS V5.1 (server-side input validation). Next.js Server Actions are public endpoints — treat their arguments as `unknown`.
Bilješke (HR)
Mapira se na OWASP A04:2021 Insecure Design i ASVS V5.1 (serverska validacija unosa). Next.js Server Actions su javni endpointi — tretiraj njihove argumente kao `unknown`.
Exceptions / Tradeoffs (EN)
None for security-relevant input. Client validation should still exist for responsiveness, but it never replaces the server check.
Iznimke / Tradeoffi (HR)
Nema iznimki za sigurnosno relevantan unos. Klijentska validacija i dalje treba postojati radi responzivnosti, ali nikad ne zamjenjuje serversku provjeru.