C.W.K.
Stream
Lesson 08 of 08 · published

Security & Best Practices

~22 min · security, CSRF, auth, closures

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

What you get for free

  • CSRF protection — Origin header validation + per-build encrypted action IDs.
  • Dead-code elimination — unused Server Actions don't end up in the build.
  • POST-only — no GET-by-URL exposure.
  • Encrypted closures — values captured by an inline action are encrypted before the action reference reaches the client.

What you must add yourself

RiskMitigation
Untrusted inputValidate every field server-side (Zod or equivalent)
Wrong user mutating someone else's dataRead session, check resource ownership before writing
Rate abuseRate-limit per IP/user (Upstash, Redis, edge middleware)
Sensitive secrets in closuresAvoid capturing secrets directly; pass identifiers and re-fetch on the server

Code

Always check auth, always validate·ts
'use server';
import { z } from 'zod';
import { auth } from '@/lib/auth';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

const Schema = z.object({ postId: z.string().uuid() });

export async function deletePost(formData: FormData) {
  const session = await auth();
  if (!session) throw new Error('Unauthorized');

  const parsed = Schema.safeParse({ postId: formData.get('postId') });
  if (!parsed.success) throw new Error('Bad input');

  const post = await db.post.findUnique({ where: { id: parsed.data.postId } });
  if (post?.authorId !== session.user.id) throw new Error('Forbidden');

  await db.post.delete({ where: { id: parsed.data.postId } });
  revalidatePath('/posts');
}
Closures are encrypted, but lean on identifiers·tsx
// Inline action capturing a secret — encrypted, but cleaner to re-fetch
export default async function Page() {
  const apiKey = await getApiKey(); // server-only

  async function callExternal(formData: FormData) {
    'use server';
    const value = formData.get('value') as string;
    await fetch('https://external/api', {
      method: 'POST',
      headers: { Authorization: `Bearer ${apiKey}` }, // captured + encrypted
      body: JSON.stringify({ value }),
    });
  }
  return <form action={callExternal}>{/* … */}</form>;
}

External links

Exercise

Audit one of your Server Actions for the four risks above. Add Zod validation, an auth check, ownership verification, and a rate-limit hook. Document the changes.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.