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
| Risk | Mitigation |
|---|---|
| Untrusted input | Validate every field server-side (Zod or equivalent) |
| Wrong user mutating someone else's data | Read session, check resource ownership before writing |
| Rate abuse | Rate-limit per IP/user (Upstash, Redis, edge middleware) |
| Sensitive secrets in closures | Avoid capturing secrets directly; pass identifiers and re-fetch on the server |
Threat-model every action as an internet-reachable POST. Test anonymous calls, another user's resource ID, oversized payloads, bursts, and stale action references. Log actor, resource, decision, and request ID while excluding secrets and raw personal input. Invoke the same action through the normal UI, a crafted POST, and sessions with different permissions. Hiding a button and having the server reject a mutation are different security proofs. Origin checks and encrypted IDs protect the framework channel; they do not know your organization roles, ownership rules, or abuse budget. A valid signed-in browser can still make an unauthorized or excessive request. Replay each hostile request without the normal UI and verify the deepest action rejects it. Repeat the suite after framework or authentication upgrades. Security that passed only once in a manual browser session is not a maintained boundary.