security-critical.mdc 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. ---
  2. title: Critical Security Patterns
  3. description: The three most important security patterns in Payload CMS
  4. tags: [payload, security, critical, access-control, transactions, hooks]
  5. priority: high
  6. ---
  7. # CRITICAL SECURITY PATTERNS
  8. These are the three most critical security patterns that MUST be followed in every Payload CMS project.
  9. ## 1. Local API Access Control (MOST IMPORTANT)
  10. **By default, Local API operations bypass ALL access control**, even when passing a user.
  11. ```typescript
  12. // ❌ SECURITY BUG: Passes user but ignores their permissions
  13. await payload.find({
  14. collection: 'posts',
  15. user: someUser, // Access control is BYPASSED!
  16. })
  17. // ✅ SECURE: Actually enforces the user's permissions
  18. await payload.find({
  19. collection: 'posts',
  20. user: someUser,
  21. overrideAccess: false, // REQUIRED for access control
  22. })
  23. // ✅ Administrative operation (intentional bypass)
  24. await payload.find({
  25. collection: 'posts',
  26. // No user, overrideAccess defaults to true
  27. })
  28. ```
  29. **When to use each:**
  30. - `overrideAccess: true` (default) - Server-side operations you trust (cron jobs, system tasks)
  31. - `overrideAccess: false` - When operating on behalf of a user (API routes, webhooks)
  32. **Rule**: When passing `user` to Local API, ALWAYS set `overrideAccess: false`
  33. ## 2. Transaction Safety in Hooks
  34. **Nested operations in hooks without `req` break transaction atomicity.**
  35. ```typescript
  36. // ❌ DATA CORRUPTION RISK: Separate transaction
  37. hooks: {
  38. afterChange: [
  39. async ({ doc, req }) => {
  40. await req.payload.create({
  41. collection: 'audit-log',
  42. data: { docId: doc.id },
  43. // Missing req - runs in separate transaction!
  44. })
  45. },
  46. ]
  47. }
  48. // ✅ ATOMIC: Same transaction
  49. hooks: {
  50. afterChange: [
  51. async ({ doc, req }) => {
  52. await req.payload.create({
  53. collection: 'audit-log',
  54. data: { docId: doc.id },
  55. req, // Maintains atomicity
  56. })
  57. },
  58. ]
  59. }
  60. ```
  61. **Why This Matters:**
  62. - **MongoDB (with replica sets)**: Creates atomic session across operations
  63. - **PostgreSQL**: All operations use same Drizzle transaction
  64. - **SQLite (with transactions enabled)**: Ensures rollback on errors
  65. - **Without req**: Each operation runs independently, breaking atomicity
  66. **Rule**: ALWAYS pass `req` to nested operations in hooks
  67. ## 3. Prevent Infinite Hook Loops
  68. **Hooks triggering operations that trigger the same hooks create infinite loops.**
  69. ```typescript
  70. // ❌ INFINITE LOOP
  71. hooks: {
  72. afterChange: [
  73. async ({ doc, req }) => {
  74. await req.payload.update({
  75. collection: 'posts',
  76. id: doc.id,
  77. data: { views: doc.views + 1 },
  78. req,
  79. }) // Triggers afterChange again!
  80. },
  81. ]
  82. }
  83. // ✅ SAFE: Use context flag
  84. hooks: {
  85. afterChange: [
  86. async ({ doc, req, context }) => {
  87. if (context.skipHooks) return
  88. await req.payload.update({
  89. collection: 'posts',
  90. id: doc.id,
  91. data: { views: doc.views + 1 },
  92. context: { skipHooks: true },
  93. req,
  94. })
  95. },
  96. ]
  97. }
  98. ```
  99. **Rule**: Use `req.context` flags to prevent hook loops