gbmake-payload/.cursor/rules/security-critical.mdc

123 lines
3.0 KiB
Plaintext

---
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