ship-safe 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -19
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/cli/bin/ship-safe.js +8 -1
- package/cli/utils/patterns.js +345 -24
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/supabase/rls-templates.sql +242 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/package.json +11 -2
- package/snippets/README.md +89 -25
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Firebase Security Checklist
|
|
2
|
+
|
|
3
|
+
**Complete this checklist before launching your Firebase-powered app.**
|
|
4
|
+
|
|
5
|
+
Based on common Firebase security vulnerabilities found in bug bounty programs and penetration tests.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Critical: Security Rules
|
|
10
|
+
|
|
11
|
+
### 1. [ ] NOT using test mode rules
|
|
12
|
+
|
|
13
|
+
**Check your Firestore rules don't contain:**
|
|
14
|
+
```javascript
|
|
15
|
+
// DANGEROUS: Test mode - allows anyone to read/write
|
|
16
|
+
allow read, write: if true;
|
|
17
|
+
|
|
18
|
+
// DANGEROUS: Time-based test mode (often forgotten)
|
|
19
|
+
allow read, write: if request.time < timestamp.date(2024, 12, 31);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Fix:** Replace with proper authentication-based rules.
|
|
23
|
+
|
|
24
|
+
### 2. [ ] Firestore rules require authentication
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
// GOOD: Requires authentication
|
|
28
|
+
allow read, write: if request.auth != null;
|
|
29
|
+
|
|
30
|
+
// BETTER: Requires authentication AND ownership
|
|
31
|
+
allow read, write: if request.auth.uid == userId;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. [ ] Storage rules have file type validation
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// GOOD: Only allow specific image types
|
|
38
|
+
allow write: if request.resource.contentType.matches('image/.*')
|
|
39
|
+
&& request.resource.contentType in ['image/jpeg', 'image/png', 'image/webp'];
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 4. [ ] Storage rules have file size limits
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// GOOD: Limit to 5MB
|
|
46
|
+
allow write: if request.resource.size < 5 * 1024 * 1024;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 5. [ ] Default deny rule at the end
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// Catch-all: deny everything not explicitly allowed
|
|
53
|
+
match /{document=**} {
|
|
54
|
+
allow read, write: if false;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Critical: API Keys
|
|
61
|
+
|
|
62
|
+
### 6. [ ] API key restrictions configured
|
|
63
|
+
|
|
64
|
+
Firebase Console > Project Settings > API Keys (in Google Cloud Console)
|
|
65
|
+
|
|
66
|
+
- [ ] Application restrictions (HTTP referrers for web, app signatures for mobile)
|
|
67
|
+
- [ ] API restrictions (only enable APIs you use)
|
|
68
|
+
|
|
69
|
+
### 7. [ ] Firebase config not containing sensitive data
|
|
70
|
+
|
|
71
|
+
Your `firebaseConfig` is designed to be public, but verify:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// These are OK to be public:
|
|
75
|
+
const firebaseConfig = {
|
|
76
|
+
apiKey: "...", // OK - restricted by security rules
|
|
77
|
+
authDomain: "...", // OK - public
|
|
78
|
+
projectId: "...", // OK - public
|
|
79
|
+
storageBucket: "...", // OK - protected by storage rules
|
|
80
|
+
messagingSenderId: "...", // OK - public
|
|
81
|
+
appId: "..." // OK - public
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// NEVER include these in client code:
|
|
85
|
+
// - Service account private keys
|
|
86
|
+
// - Admin SDK credentials
|
|
87
|
+
// - Database URLs with embedded secrets
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 8. [ ] Service account keys not in client code
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Scan for service account files
|
|
94
|
+
npx ship-safe scan .
|
|
95
|
+
|
|
96
|
+
# Or manually search
|
|
97
|
+
find . -name "*.json" -exec grep -l "private_key" {} \;
|
|
98
|
+
grep -r "BEGIN PRIVATE KEY" .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## High: Authentication
|
|
104
|
+
|
|
105
|
+
### 9. [ ] Email enumeration protection enabled
|
|
106
|
+
|
|
107
|
+
Firebase Console > Authentication > Settings > User Actions
|
|
108
|
+
- [ ] Email enumeration protection = ON
|
|
109
|
+
|
|
110
|
+
This prevents attackers from discovering valid email addresses.
|
|
111
|
+
|
|
112
|
+
### 10. [ ] Password requirements configured
|
|
113
|
+
|
|
114
|
+
Firebase Console > Authentication > Sign-in method > Email/Password
|
|
115
|
+
- [ ] Minimum password length (recommend 8+)
|
|
116
|
+
- [ ] Require uppercase/lowercase/numbers (if supported)
|
|
117
|
+
|
|
118
|
+
### 11. [ ] OAuth providers properly configured
|
|
119
|
+
|
|
120
|
+
For each OAuth provider (Google, Facebook, etc.):
|
|
121
|
+
- [ ] Authorized domains list is correct
|
|
122
|
+
- [ ] Callback URLs are HTTPS only
|
|
123
|
+
- [ ] Client secrets are not exposed
|
|
124
|
+
|
|
125
|
+
### 12. [ ] Phone auth abuse protection
|
|
126
|
+
|
|
127
|
+
If using phone authentication:
|
|
128
|
+
- [ ] SMS region policy configured (limit to countries you serve)
|
|
129
|
+
- [ ] App verification enabled
|
|
130
|
+
- [ ] Rate limiting understood
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## High: Database Security
|
|
135
|
+
|
|
136
|
+
### 13. [ ] No sensitive data in public collections
|
|
137
|
+
|
|
138
|
+
Review your data structure:
|
|
139
|
+
- [ ] User emails not in publicly readable collections
|
|
140
|
+
- [ ] Payment info not in Firestore (use Stripe)
|
|
141
|
+
- [ ] Passwords never stored (use Firebase Auth)
|
|
142
|
+
|
|
143
|
+
### 14. [ ] Indexes don't expose data patterns
|
|
144
|
+
|
|
145
|
+
Firebase Console > Firestore > Indexes
|
|
146
|
+
|
|
147
|
+
Complex indexes can reveal data structure. Review each index.
|
|
148
|
+
|
|
149
|
+
### 15. [ ] Backup and recovery plan exists
|
|
150
|
+
|
|
151
|
+
Firebase Console > Firestore > Backups
|
|
152
|
+
- [ ] Automated backups enabled
|
|
153
|
+
- [ ] Tested restore process
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Medium: Monitoring & Alerts
|
|
158
|
+
|
|
159
|
+
### 16. [ ] Firebase App Check enabled
|
|
160
|
+
|
|
161
|
+
Firebase Console > App Check
|
|
162
|
+
- [ ] reCAPTCHA for web
|
|
163
|
+
- [ ] Device Check for iOS
|
|
164
|
+
- [ ] Play Integrity for Android
|
|
165
|
+
|
|
166
|
+
App Check helps prevent abuse from unauthorized clients.
|
|
167
|
+
|
|
168
|
+
### 17. [ ] Budget alerts configured
|
|
169
|
+
|
|
170
|
+
Google Cloud Console > Billing > Budgets & alerts
|
|
171
|
+
- [ ] Budget set for expected usage
|
|
172
|
+
- [ ] Alerts at 50%, 90%, 100%
|
|
173
|
+
|
|
174
|
+
Prevents surprise bills from attacks or bugs.
|
|
175
|
+
|
|
176
|
+
### 18. [ ] Security rules monitoring
|
|
177
|
+
|
|
178
|
+
Firebase Console > Firestore > Rules > Monitor
|
|
179
|
+
- [ ] Review denied requests regularly
|
|
180
|
+
- [ ] Set up alerts for unusual patterns
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Quick Security Audit Commands
|
|
185
|
+
|
|
186
|
+
### Check for exposed Firebase configs
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Search for Firebase config in your codebase
|
|
190
|
+
grep -r "firebaseConfig" --include="*.js" --include="*.ts" .
|
|
191
|
+
grep -r "apiKey.*firebase" --include="*.js" --include="*.ts" .
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Test your security rules
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Install Firebase Emulator
|
|
198
|
+
npm install -g firebase-tools
|
|
199
|
+
|
|
200
|
+
# Run security rules tests
|
|
201
|
+
firebase emulators:start --only firestore
|
|
202
|
+
# Then run your test suite against localhost
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Scan for secrets
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx ship-safe scan .
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Testing Checklist
|
|
214
|
+
|
|
215
|
+
Before launch, test these scenarios:
|
|
216
|
+
|
|
217
|
+
1. [ ] Unauthenticated user cannot read private data
|
|
218
|
+
2. [ ] User A cannot read User B's private data
|
|
219
|
+
3. [ ] User cannot write to another user's document
|
|
220
|
+
4. [ ] File uploads are rejected if wrong type
|
|
221
|
+
5. [ ] Large file uploads are rejected
|
|
222
|
+
6. [ ] Rate limiting works (if implemented)
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Firebase Security Resources
|
|
227
|
+
|
|
228
|
+
- [Firebase Security Rules Documentation](https://firebase.google.com/docs/rules)
|
|
229
|
+
- [Firebase Security Checklist](https://firebase.google.com/support/guides/security-checklist)
|
|
230
|
+
- [Firebase App Check](https://firebase.google.com/docs/app-check)
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
**Remember: Firebase makes it easy to build fast, but "test mode" is not a security strategy.**
|
|
235
|
+
|
|
236
|
+
Run `npx ship-safe scan .` to check for leaked keys before every deploy.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// FIREBASE STORAGE SECURITY RULES TEMPLATES
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Copy these rules to your Firebase Console > Storage > Rules
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS MATTERS:
|
|
8
|
+
// - Default rules may allow anyone to upload/download files
|
|
9
|
+
// - Uploaded files can contain malware or illegal content
|
|
10
|
+
// - Storage costs can explode if not properly secured
|
|
11
|
+
//
|
|
12
|
+
// HOW TO USE:
|
|
13
|
+
// 1. Go to Firebase Console > Storage > Rules
|
|
14
|
+
// 2. Replace the default rules with these templates
|
|
15
|
+
// 3. Customize paths and limits for your app
|
|
16
|
+
// 4. Publish and test thoroughly
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
rules_version = '2';
|
|
21
|
+
service firebase.storage {
|
|
22
|
+
match /b/{bucket}/o {
|
|
23
|
+
|
|
24
|
+
// =========================================================================
|
|
25
|
+
// HELPER FUNCTIONS
|
|
26
|
+
// =========================================================================
|
|
27
|
+
|
|
28
|
+
// Check if user is authenticated
|
|
29
|
+
function isAuthenticated() {
|
|
30
|
+
return request.auth != null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get the authenticated user's ID
|
|
34
|
+
function userId() {
|
|
35
|
+
return request.auth.uid;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check file size (in bytes)
|
|
39
|
+
function isUnderMaxSize(maxSizeMB) {
|
|
40
|
+
return request.resource.size < maxSizeMB * 1024 * 1024;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if file is an image
|
|
44
|
+
function isImage() {
|
|
45
|
+
return request.resource.contentType.matches('image/.*');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if file is a specific image type
|
|
49
|
+
function isImageType(types) {
|
|
50
|
+
return request.resource.contentType in types;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if file is a document
|
|
54
|
+
function isDocument() {
|
|
55
|
+
return request.resource.contentType in [
|
|
56
|
+
'application/pdf',
|
|
57
|
+
'application/msword',
|
|
58
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =========================================================================
|
|
63
|
+
// PATTERN 1: USER AVATARS (User's own files)
|
|
64
|
+
// =========================================================================
|
|
65
|
+
|
|
66
|
+
match /avatars/{userId}/{fileName} {
|
|
67
|
+
// Anyone can view avatars (they're public profile pictures)
|
|
68
|
+
allow read: if true;
|
|
69
|
+
|
|
70
|
+
// Users can only upload to their own folder
|
|
71
|
+
allow write: if isAuthenticated()
|
|
72
|
+
&& request.auth.uid == userId
|
|
73
|
+
&& isImage()
|
|
74
|
+
&& isUnderMaxSize(2) // 2MB max
|
|
75
|
+
&& isImageType(['image/jpeg', 'image/png', 'image/webp']);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =========================================================================
|
|
79
|
+
// PATTERN 2: PRIVATE USER FILES
|
|
80
|
+
// =========================================================================
|
|
81
|
+
|
|
82
|
+
match /users/{userId}/private/{allPaths=**} {
|
|
83
|
+
// Users can only access their own private files
|
|
84
|
+
allow read: if isAuthenticated() && request.auth.uid == userId;
|
|
85
|
+
|
|
86
|
+
// Users can upload to their own folder with size limits
|
|
87
|
+
allow write: if isAuthenticated()
|
|
88
|
+
&& request.auth.uid == userId
|
|
89
|
+
&& isUnderMaxSize(10); // 10MB max
|
|
90
|
+
|
|
91
|
+
// Users can delete their own files
|
|
92
|
+
allow delete: if isAuthenticated() && request.auth.uid == userId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =========================================================================
|
|
96
|
+
// PATTERN 3: SHARED/PUBLIC FILES (Authenticated upload, public read)
|
|
97
|
+
// =========================================================================
|
|
98
|
+
|
|
99
|
+
match /public/{allPaths=**} {
|
|
100
|
+
// Anyone can read public files
|
|
101
|
+
allow read: if true;
|
|
102
|
+
|
|
103
|
+
// Only authenticated users can upload
|
|
104
|
+
allow write: if isAuthenticated()
|
|
105
|
+
&& isUnderMaxSize(5)
|
|
106
|
+
&& (isImage() || isDocument());
|
|
107
|
+
|
|
108
|
+
// Only admins can delete (implement admin check via custom claims)
|
|
109
|
+
allow delete: if request.auth.token.admin == true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =========================================================================
|
|
113
|
+
// PATTERN 4: ORGANIZATION FILES
|
|
114
|
+
// =========================================================================
|
|
115
|
+
|
|
116
|
+
match /organizations/{orgId}/{allPaths=**} {
|
|
117
|
+
// Check org membership via custom claims
|
|
118
|
+
function isOrgMember() {
|
|
119
|
+
return request.auth.token.orgId == orgId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isOrgAdmin() {
|
|
123
|
+
return request.auth.token.orgId == orgId
|
|
124
|
+
&& request.auth.token.orgRole == 'admin';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Org members can read
|
|
128
|
+
allow read: if isAuthenticated() && isOrgMember();
|
|
129
|
+
|
|
130
|
+
// Org members can upload
|
|
131
|
+
allow create: if isAuthenticated()
|
|
132
|
+
&& isOrgMember()
|
|
133
|
+
&& isUnderMaxSize(50); // 50MB for org files
|
|
134
|
+
|
|
135
|
+
// Org admins can delete
|
|
136
|
+
allow delete: if isAuthenticated() && isOrgAdmin();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =========================================================================
|
|
140
|
+
// PATTERN 5: TEMPORARY UPLOADS (with expiration)
|
|
141
|
+
// =========================================================================
|
|
142
|
+
|
|
143
|
+
match /temp/{uploadId} {
|
|
144
|
+
// Anyone authenticated can upload to temp
|
|
145
|
+
allow create: if isAuthenticated()
|
|
146
|
+
&& isUnderMaxSize(10)
|
|
147
|
+
// Set metadata for cleanup jobs
|
|
148
|
+
&& request.resource.metadata.uploadedBy == userId()
|
|
149
|
+
&& request.resource.metadata.expiresAt != null;
|
|
150
|
+
|
|
151
|
+
// Only the uploader can read/delete their temp files
|
|
152
|
+
allow read, delete: if isAuthenticated()
|
|
153
|
+
&& resource.metadata.uploadedBy == userId();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// PATTERN 6: STRICT IMAGE UPLOADS (Profile pictures, etc.)
|
|
158
|
+
// =========================================================================
|
|
159
|
+
|
|
160
|
+
match /images/{imageId} {
|
|
161
|
+
// Public read
|
|
162
|
+
allow read: if true;
|
|
163
|
+
|
|
164
|
+
// Strict upload requirements
|
|
165
|
+
allow create: if isAuthenticated()
|
|
166
|
+
// Must be an allowed image type
|
|
167
|
+
&& isImageType(['image/jpeg', 'image/png', 'image/gif', 'image/webp'])
|
|
168
|
+
// Max 5MB
|
|
169
|
+
&& isUnderMaxSize(5)
|
|
170
|
+
// Must have dimensions metadata (set by client)
|
|
171
|
+
&& request.resource.metadata.width != null
|
|
172
|
+
&& request.resource.metadata.height != null
|
|
173
|
+
// Reasonable dimensions (prevent huge images)
|
|
174
|
+
&& int(request.resource.metadata.width) <= 4096
|
|
175
|
+
&& int(request.resource.metadata.height) <= 4096;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// =========================================================================
|
|
179
|
+
// ANTI-PATTERNS: NEVER DO THIS
|
|
180
|
+
// =========================================================================
|
|
181
|
+
|
|
182
|
+
// BAD: Allows anyone to read/write everything
|
|
183
|
+
// match /{allPaths=**} {
|
|
184
|
+
// allow read, write: if true;
|
|
185
|
+
// }
|
|
186
|
+
|
|
187
|
+
// BAD: No file type validation (allows malware uploads)
|
|
188
|
+
// match /uploads/{file} {
|
|
189
|
+
// allow write: if request.auth != null;
|
|
190
|
+
// }
|
|
191
|
+
|
|
192
|
+
// BAD: No size limits (allows storage cost attacks)
|
|
193
|
+
// match /files/{file} {
|
|
194
|
+
// allow write: if request.auth != null;
|
|
195
|
+
// }
|
|
196
|
+
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// DEFAULT: DENY ALL (Safety net)
|
|
199
|
+
// =========================================================================
|
|
200
|
+
|
|
201
|
+
// Deny access to any path not explicitly defined above
|
|
202
|
+
match /{allPaths=**} {
|
|
203
|
+
allow read, write: if false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- SUPABASE ROW LEVEL SECURITY (RLS) TEMPLATES
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
--
|
|
5
|
+
-- Copy-paste these policies to secure your Supabase tables.
|
|
6
|
+
--
|
|
7
|
+
-- WHY RLS MATTERS:
|
|
8
|
+
-- Without RLS, anyone with your anon key can read/write ALL data.
|
|
9
|
+
-- 83% of exposed Supabase databases have RLS misconfigurations.
|
|
10
|
+
-- Source: https://byteiota.com/supabase-security-flaw-170-apps-exposed-by-missing-rls/
|
|
11
|
+
--
|
|
12
|
+
-- HOW TO USE:
|
|
13
|
+
-- 1. Enable RLS on your table: ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
14
|
+
-- 2. Copy the relevant policy below
|
|
15
|
+
-- 3. Replace 'your_table' with your actual table name
|
|
16
|
+
-- 4. Test with different user contexts
|
|
17
|
+
--
|
|
18
|
+
-- =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
-- =============================================================================
|
|
22
|
+
-- STEP 1: ALWAYS ENABLE RLS FIRST
|
|
23
|
+
-- =============================================================================
|
|
24
|
+
|
|
25
|
+
-- Enable RLS on a table (REQUIRED before adding policies)
|
|
26
|
+
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
27
|
+
|
|
28
|
+
-- Force RLS for table owners too (recommended for security)
|
|
29
|
+
ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
-- =============================================================================
|
|
33
|
+
-- PATTERN 1: USER OWNS THEIR DATA
|
|
34
|
+
-- =============================================================================
|
|
35
|
+
-- Use when: Each user should only see/edit their own records
|
|
36
|
+
-- Example tables: profiles, settings, user_preferences
|
|
37
|
+
|
|
38
|
+
-- Users can only SELECT their own rows
|
|
39
|
+
CREATE POLICY "Users can view own data"
|
|
40
|
+
ON your_table
|
|
41
|
+
FOR SELECT
|
|
42
|
+
USING (auth.uid() = user_id);
|
|
43
|
+
|
|
44
|
+
-- Users can only INSERT rows with their own user_id
|
|
45
|
+
CREATE POLICY "Users can insert own data"
|
|
46
|
+
ON your_table
|
|
47
|
+
FOR INSERT
|
|
48
|
+
WITH CHECK (auth.uid() = user_id);
|
|
49
|
+
|
|
50
|
+
-- Users can only UPDATE their own rows
|
|
51
|
+
CREATE POLICY "Users can update own data"
|
|
52
|
+
ON your_table
|
|
53
|
+
FOR UPDATE
|
|
54
|
+
USING (auth.uid() = user_id)
|
|
55
|
+
WITH CHECK (auth.uid() = user_id);
|
|
56
|
+
|
|
57
|
+
-- Users can only DELETE their own rows
|
|
58
|
+
CREATE POLICY "Users can delete own data"
|
|
59
|
+
ON your_table
|
|
60
|
+
FOR DELETE
|
|
61
|
+
USING (auth.uid() = user_id);
|
|
62
|
+
|
|
63
|
+
-- COMBINED: All operations for own data (simpler but less granular)
|
|
64
|
+
CREATE POLICY "Users manage own data"
|
|
65
|
+
ON your_table
|
|
66
|
+
FOR ALL
|
|
67
|
+
USING (auth.uid() = user_id)
|
|
68
|
+
WITH CHECK (auth.uid() = user_id);
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
-- =============================================================================
|
|
72
|
+
-- PATTERN 2: ORGANIZATION/TEAM BASED ACCESS
|
|
73
|
+
-- =============================================================================
|
|
74
|
+
-- Use when: Users belong to orgs and should see all org data
|
|
75
|
+
-- Example tables: projects, documents, team_settings
|
|
76
|
+
|
|
77
|
+
-- First, create a helper function to get user's org
|
|
78
|
+
CREATE OR REPLACE FUNCTION get_user_org_id()
|
|
79
|
+
RETURNS UUID AS $$
|
|
80
|
+
SELECT org_id FROM profiles WHERE id = auth.uid()
|
|
81
|
+
$$ LANGUAGE SQL SECURITY DEFINER;
|
|
82
|
+
|
|
83
|
+
-- Users can view all data in their organization
|
|
84
|
+
CREATE POLICY "Org members can view org data"
|
|
85
|
+
ON your_table
|
|
86
|
+
FOR SELECT
|
|
87
|
+
USING (org_id = get_user_org_id());
|
|
88
|
+
|
|
89
|
+
-- Users can insert data into their organization
|
|
90
|
+
CREATE POLICY "Org members can insert org data"
|
|
91
|
+
ON your_table
|
|
92
|
+
FOR INSERT
|
|
93
|
+
WITH CHECK (org_id = get_user_org_id());
|
|
94
|
+
|
|
95
|
+
-- Only allow updates to org data
|
|
96
|
+
CREATE POLICY "Org members can update org data"
|
|
97
|
+
ON your_table
|
|
98
|
+
FOR UPDATE
|
|
99
|
+
USING (org_id = get_user_org_id())
|
|
100
|
+
WITH CHECK (org_id = get_user_org_id());
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
-- =============================================================================
|
|
104
|
+
-- PATTERN 3: ROLE-BASED ACCESS CONTROL (RBAC)
|
|
105
|
+
-- =============================================================================
|
|
106
|
+
-- Use when: Different users have different permission levels
|
|
107
|
+
-- Example: admin, editor, viewer roles
|
|
108
|
+
|
|
109
|
+
-- Helper function to check user role
|
|
110
|
+
CREATE OR REPLACE FUNCTION get_user_role()
|
|
111
|
+
RETURNS TEXT AS $$
|
|
112
|
+
SELECT role FROM profiles WHERE id = auth.uid()
|
|
113
|
+
$$ LANGUAGE SQL SECURITY DEFINER;
|
|
114
|
+
|
|
115
|
+
-- Anyone authenticated can view (public within app)
|
|
116
|
+
CREATE POLICY "Authenticated users can view"
|
|
117
|
+
ON your_table
|
|
118
|
+
FOR SELECT
|
|
119
|
+
USING (auth.role() = 'authenticated');
|
|
120
|
+
|
|
121
|
+
-- Only admins and editors can insert
|
|
122
|
+
CREATE POLICY "Admins and editors can insert"
|
|
123
|
+
ON your_table
|
|
124
|
+
FOR INSERT
|
|
125
|
+
WITH CHECK (get_user_role() IN ('admin', 'editor'));
|
|
126
|
+
|
|
127
|
+
-- Only admins can delete
|
|
128
|
+
CREATE POLICY "Only admins can delete"
|
|
129
|
+
ON your_table
|
|
130
|
+
FOR DELETE
|
|
131
|
+
USING (get_user_role() = 'admin');
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
-- =============================================================================
|
|
135
|
+
-- PATTERN 4: PUBLIC READ, AUTHENTICATED WRITE
|
|
136
|
+
-- =============================================================================
|
|
137
|
+
-- Use when: Content is public but only logged-in users can contribute
|
|
138
|
+
-- Example tables: blog_posts, comments, public_profiles
|
|
139
|
+
|
|
140
|
+
-- Anyone can read (including anonymous)
|
|
141
|
+
CREATE POLICY "Public read access"
|
|
142
|
+
ON your_table
|
|
143
|
+
FOR SELECT
|
|
144
|
+
USING (true);
|
|
145
|
+
|
|
146
|
+
-- Only authenticated users can insert
|
|
147
|
+
CREATE POLICY "Authenticated users can insert"
|
|
148
|
+
ON your_table
|
|
149
|
+
FOR INSERT
|
|
150
|
+
WITH CHECK (auth.role() = 'authenticated');
|
|
151
|
+
|
|
152
|
+
-- Users can only update their own posts
|
|
153
|
+
CREATE POLICY "Users can update own posts"
|
|
154
|
+
ON your_table
|
|
155
|
+
FOR UPDATE
|
|
156
|
+
USING (auth.uid() = author_id)
|
|
157
|
+
WITH CHECK (auth.uid() = author_id);
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
-- =============================================================================
|
|
161
|
+
-- PATTERN 5: PRIVATE BY DEFAULT (Explicit sharing)
|
|
162
|
+
-- =============================================================================
|
|
163
|
+
-- Use when: Data is private unless explicitly shared
|
|
164
|
+
-- Example tables: documents, files with sharing
|
|
165
|
+
|
|
166
|
+
-- Owner can always access
|
|
167
|
+
CREATE POLICY "Owner full access"
|
|
168
|
+
ON documents
|
|
169
|
+
FOR ALL
|
|
170
|
+
USING (auth.uid() = owner_id)
|
|
171
|
+
WITH CHECK (auth.uid() = owner_id);
|
|
172
|
+
|
|
173
|
+
-- Shared users can view (requires a shares table)
|
|
174
|
+
CREATE POLICY "Shared users can view"
|
|
175
|
+
ON documents
|
|
176
|
+
FOR SELECT
|
|
177
|
+
USING (
|
|
178
|
+
auth.uid() = owner_id
|
|
179
|
+
OR
|
|
180
|
+
EXISTS (
|
|
181
|
+
SELECT 1 FROM document_shares
|
|
182
|
+
WHERE document_shares.document_id = documents.id
|
|
183
|
+
AND document_shares.user_id = auth.uid()
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
-- =============================================================================
|
|
189
|
+
-- PATTERN 6: TIME-BASED ACCESS
|
|
190
|
+
-- =============================================================================
|
|
191
|
+
-- Use when: Content has publish dates or expiration
|
|
192
|
+
-- Example tables: scheduled_posts, limited_offers
|
|
193
|
+
|
|
194
|
+
-- Only show published content
|
|
195
|
+
CREATE POLICY "Show only published content"
|
|
196
|
+
ON posts
|
|
197
|
+
FOR SELECT
|
|
198
|
+
USING (
|
|
199
|
+
published_at IS NOT NULL
|
|
200
|
+
AND published_at <= NOW()
|
|
201
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
-- Authors can always see their drafts
|
|
205
|
+
CREATE POLICY "Authors see own drafts"
|
|
206
|
+
ON posts
|
|
207
|
+
FOR SELECT
|
|
208
|
+
USING (auth.uid() = author_id);
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
-- =============================================================================
|
|
212
|
+
-- ANTI-PATTERNS: DON'T DO THIS
|
|
213
|
+
-- =============================================================================
|
|
214
|
+
|
|
215
|
+
-- BAD: Allows anyone to read everything
|
|
216
|
+
-- CREATE POLICY "bad_policy" ON users FOR SELECT USING (true);
|
|
217
|
+
|
|
218
|
+
-- BAD: No WITH CHECK means users could insert data for other users
|
|
219
|
+
-- CREATE POLICY "bad_insert" ON posts FOR INSERT WITH CHECK (true);
|
|
220
|
+
|
|
221
|
+
-- BAD: Missing USING clause on UPDATE allows updating any row
|
|
222
|
+
-- CREATE POLICY "bad_update" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
-- =============================================================================
|
|
226
|
+
-- TESTING YOUR POLICIES
|
|
227
|
+
-- =============================================================================
|
|
228
|
+
|
|
229
|
+
-- Test as a specific user (in SQL editor)
|
|
230
|
+
-- SET request.jwt.claim.sub = 'user-uuid-here';
|
|
231
|
+
-- SELECT * FROM your_table;
|
|
232
|
+
|
|
233
|
+
-- Check existing policies on a table
|
|
234
|
+
SELECT * FROM pg_policies WHERE tablename = 'your_table';
|
|
235
|
+
|
|
236
|
+
-- List all tables without RLS enabled (DANGER!)
|
|
237
|
+
SELECT schemaname, tablename
|
|
238
|
+
FROM pg_tables
|
|
239
|
+
WHERE schemaname = 'public'
|
|
240
|
+
AND tablename NOT IN (
|
|
241
|
+
SELECT tablename FROM pg_policies
|
|
242
|
+
);
|