ship-safe 1.0.1 → 3.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 +281 -23
- 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 +44 -2
- package/cli/commands/fix.js +216 -0
- package/cli/commands/guard.js +297 -0
- package/cli/commands/mcp.js +303 -0
- package/cli/commands/scan.js +231 -39
- package/cli/utils/entropy.js +126 -0
- package/cli/utils/output.js +10 -1
- package/cli/utils/patterns.js +376 -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/ship-safeignore-template +50 -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,215 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// FIREBASE FIRESTORE SECURITY RULES TEMPLATES
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Copy these rules to your Firebase Console > Firestore > Rules
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS MATTERS:
|
|
8
|
+
// - Default test mode rules allow ANYONE to read/write ALL data
|
|
9
|
+
// - Test mode is the #1 cause of Firebase data breaches
|
|
10
|
+
// - 40% of Firebase apps have misconfigured security rules
|
|
11
|
+
//
|
|
12
|
+
// HOW TO USE:
|
|
13
|
+
// 1. Go to Firebase Console > Firestore Database > Rules
|
|
14
|
+
// 2. Replace the default rules with these templates
|
|
15
|
+
// 3. Customize for your data structure
|
|
16
|
+
// 4. Publish and test thoroughly
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
rules_version = '2';
|
|
21
|
+
service cloud.firestore {
|
|
22
|
+
match /databases/{database}/documents {
|
|
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 if user owns a document (document has a userId field)
|
|
39
|
+
function isOwner() {
|
|
40
|
+
return isAuthenticated() && resource.data.userId == userId();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if user is creating a document with their own userId
|
|
44
|
+
function isCreatingOwn() {
|
|
45
|
+
return isAuthenticated() && request.resource.data.userId == userId();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if user has a specific role (requires a users collection with role field)
|
|
49
|
+
function hasRole(role) {
|
|
50
|
+
return isAuthenticated() &&
|
|
51
|
+
get(/databases/$(database)/documents/users/$(userId())).data.role == role;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if user is admin
|
|
55
|
+
function isAdmin() {
|
|
56
|
+
return hasRole('admin');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate that required fields exist
|
|
60
|
+
function hasRequiredFields(fields) {
|
|
61
|
+
return request.resource.data.keys().hasAll(fields);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if only allowed fields are being modified
|
|
65
|
+
function onlyUpdating(fields) {
|
|
66
|
+
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// =========================================================================
|
|
70
|
+
// PATTERN 1: USER PROFILES (User owns their profile)
|
|
71
|
+
// =========================================================================
|
|
72
|
+
|
|
73
|
+
match /users/{userId} {
|
|
74
|
+
// Users can read their own profile
|
|
75
|
+
allow read: if isAuthenticated() && request.auth.uid == userId;
|
|
76
|
+
|
|
77
|
+
// Users can create their own profile
|
|
78
|
+
allow create: if isAuthenticated()
|
|
79
|
+
&& request.auth.uid == userId
|
|
80
|
+
&& hasRequiredFields(['email', 'createdAt'])
|
|
81
|
+
&& request.resource.data.createdAt == request.time;
|
|
82
|
+
|
|
83
|
+
// Users can update their own profile (except role)
|
|
84
|
+
allow update: if isAuthenticated()
|
|
85
|
+
&& request.auth.uid == userId
|
|
86
|
+
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
|
|
87
|
+
|
|
88
|
+
// Only admins can delete users
|
|
89
|
+
allow delete: if isAdmin();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =========================================================================
|
|
93
|
+
// PATTERN 2: PRIVATE DATA (User's own data)
|
|
94
|
+
// =========================================================================
|
|
95
|
+
|
|
96
|
+
match /private/{userId}/{document=**} {
|
|
97
|
+
// Users can only access their own private data
|
|
98
|
+
allow read, write: if isAuthenticated() && request.auth.uid == userId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// PATTERN 3: POSTS/CONTENT (Public read, authenticated write)
|
|
103
|
+
// =========================================================================
|
|
104
|
+
|
|
105
|
+
match /posts/{postId} {
|
|
106
|
+
// Anyone can read published posts
|
|
107
|
+
allow read: if resource.data.published == true
|
|
108
|
+
|| (isAuthenticated() && resource.data.authorId == userId());
|
|
109
|
+
|
|
110
|
+
// Authenticated users can create posts
|
|
111
|
+
allow create: if isAuthenticated()
|
|
112
|
+
&& request.resource.data.authorId == userId()
|
|
113
|
+
&& hasRequiredFields(['title', 'content', 'authorId', 'createdAt'])
|
|
114
|
+
&& request.resource.data.createdAt == request.time;
|
|
115
|
+
|
|
116
|
+
// Authors can update their own posts
|
|
117
|
+
allow update: if isAuthenticated()
|
|
118
|
+
&& resource.data.authorId == userId()
|
|
119
|
+
&& request.resource.data.authorId == userId(); // Can't change author
|
|
120
|
+
|
|
121
|
+
// Authors and admins can delete
|
|
122
|
+
allow delete: if isAuthenticated()
|
|
123
|
+
&& (resource.data.authorId == userId() || isAdmin());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =========================================================================
|
|
127
|
+
// PATTERN 4: ORGANIZATION/TEAM DATA
|
|
128
|
+
// =========================================================================
|
|
129
|
+
|
|
130
|
+
match /organizations/{orgId} {
|
|
131
|
+
// Function to check org membership
|
|
132
|
+
function isOrgMember() {
|
|
133
|
+
return isAuthenticated() &&
|
|
134
|
+
exists(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId()));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isOrgAdmin() {
|
|
138
|
+
return isAuthenticated() &&
|
|
139
|
+
get(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId())).data.role == 'admin';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Org members can read org data
|
|
143
|
+
allow read: if isOrgMember();
|
|
144
|
+
|
|
145
|
+
// Only org admins can update org settings
|
|
146
|
+
allow update: if isOrgAdmin();
|
|
147
|
+
|
|
148
|
+
// Nested collection: org members
|
|
149
|
+
match /members/{memberId} {
|
|
150
|
+
allow read: if isOrgMember();
|
|
151
|
+
allow write: if isOrgAdmin();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Nested collection: org projects
|
|
155
|
+
match /projects/{projectId} {
|
|
156
|
+
allow read: if isOrgMember();
|
|
157
|
+
allow create: if isOrgMember()
|
|
158
|
+
&& request.resource.data.createdBy == userId();
|
|
159
|
+
allow update: if isOrgMember();
|
|
160
|
+
allow delete: if isOrgAdmin();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// PATTERN 5: RATE LIMITING (Using timestamps)
|
|
166
|
+
// =========================================================================
|
|
167
|
+
|
|
168
|
+
match /rateLimited/{docId} {
|
|
169
|
+
// Allow creation only if user hasn't created one in the last minute
|
|
170
|
+
allow create: if isAuthenticated()
|
|
171
|
+
&& (!exists(/databases/$(database)/documents/rateLimited/$(userId()))
|
|
172
|
+
|| get(/databases/$(database)/documents/rateLimited/$(userId())).data.lastAction < request.time - duration.value(1, 'm'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// =========================================================================
|
|
176
|
+
// PATTERN 6: READ-ONLY PUBLIC DATA
|
|
177
|
+
// =========================================================================
|
|
178
|
+
|
|
179
|
+
match /public/{document=**} {
|
|
180
|
+
// Anyone can read public data
|
|
181
|
+
allow read: if true;
|
|
182
|
+
|
|
183
|
+
// Only admins can write
|
|
184
|
+
allow write: if isAdmin();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =========================================================================
|
|
188
|
+
// ANTI-PATTERNS: NEVER DO THIS
|
|
189
|
+
// =========================================================================
|
|
190
|
+
|
|
191
|
+
// BAD: Allows anyone to read/write everything
|
|
192
|
+
// match /{document=**} {
|
|
193
|
+
// allow read, write: if true;
|
|
194
|
+
// }
|
|
195
|
+
|
|
196
|
+
// BAD: Test mode rules (auto-generated, REMOVE before production)
|
|
197
|
+
// match /{document=**} {
|
|
198
|
+
// allow read, write: if request.time < timestamp.date(2024, 12, 31);
|
|
199
|
+
// }
|
|
200
|
+
|
|
201
|
+
// BAD: Checking auth but not ownership
|
|
202
|
+
// match /users/{userId} {
|
|
203
|
+
// allow read, write: if request.auth != null; // Any user can access any user!
|
|
204
|
+
// }
|
|
205
|
+
|
|
206
|
+
// =========================================================================
|
|
207
|
+
// DEFAULT: DENY ALL (Safety net)
|
|
208
|
+
// =========================================================================
|
|
209
|
+
|
|
210
|
+
// Deny access to any collection not explicitly defined above
|
|
211
|
+
match /{document=**} {
|
|
212
|
+
allow read, write: if false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -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
|
+
}
|