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.
@@ -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
+ }