| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- ---
- title: Critical Security Patterns
- description: The three most important security patterns in Payload CMS
- tags: [payload, security, critical, access-control, transactions, hooks]
- priority: high
- ---
- # CRITICAL SECURITY PATTERNS
- These are the three most critical security patterns that MUST be followed in every Payload CMS project.
- ## 1. Local API Access Control (MOST IMPORTANT)
- **By default, Local API operations bypass ALL access control**, even when passing a user.
- ```typescript
- // ❌ SECURITY BUG: Passes user but ignores their permissions
- await payload.find({
- collection: 'posts',
- user: someUser, // Access control is BYPASSED!
- })
- // ✅ SECURE: Actually enforces the user's permissions
- await payload.find({
- collection: 'posts',
- user: someUser,
- overrideAccess: false, // REQUIRED for access control
- })
- // ✅ Administrative operation (intentional bypass)
- await payload.find({
- collection: 'posts',
- // No user, overrideAccess defaults to true
- })
- ```
- **When to use each:**
- - `overrideAccess: true` (default) - Server-side operations you trust (cron jobs, system tasks)
- - `overrideAccess: false` - When operating on behalf of a user (API routes, webhooks)
- **Rule**: When passing `user` to Local API, ALWAYS set `overrideAccess: false`
- ## 2. Transaction Safety in Hooks
- **Nested operations in hooks without `req` break transaction atomicity.**
- ```typescript
- // ❌ DATA CORRUPTION RISK: Separate transaction
- hooks: {
- afterChange: [
- async ({ doc, req }) => {
- await req.payload.create({
- collection: 'audit-log',
- data: { docId: doc.id },
- // Missing req - runs in separate transaction!
- })
- },
- ]
- }
- // ✅ ATOMIC: Same transaction
- hooks: {
- afterChange: [
- async ({ doc, req }) => {
- await req.payload.create({
- collection: 'audit-log',
- data: { docId: doc.id },
- req, // Maintains atomicity
- })
- },
- ]
- }
- ```
- **Why This Matters:**
- - **MongoDB (with replica sets)**: Creates atomic session across operations
- - **PostgreSQL**: All operations use same Drizzle transaction
- - **SQLite (with transactions enabled)**: Ensures rollback on errors
- - **Without req**: Each operation runs independently, breaking atomicity
- **Rule**: ALWAYS pass `req` to nested operations in hooks
- ## 3. Prevent Infinite Hook Loops
- **Hooks triggering operations that trigger the same hooks create infinite loops.**
- ```typescript
- // ❌ INFINITE LOOP
- hooks: {
- afterChange: [
- async ({ doc, req }) => {
- await req.payload.update({
- collection: 'posts',
- id: doc.id,
- data: { views: doc.views + 1 },
- req,
- }) // Triggers afterChange again!
- },
- ]
- }
- // ✅ SAFE: Use context flag
- hooks: {
- afterChange: [
- async ({ doc, req, context }) => {
- if (context.skipHooks) return
- await req.payload.update({
- collection: 'posts',
- id: doc.id,
- data: { views: doc.views + 1 },
- context: { skipHooks: true },
- req,
- })
- },
- ]
- }
- ```
- **Rule**: Use `req.context` flags to prevent hook loops
|