Firebase Security Rules - Proposed Solution
Executive Summary
This document outlines the comprehensive solution to fix the critical security vulnerabilities in the Firebase Realtime Database. The solution implements company isolation, role-based access control, and user data protection while maintaining application functionality.
Implementation Timeline: 5-7 days Effort Estimate: 17-28 hours Risk Level: Medium (with proper testing and staged rollout)
Security Principles
The new security rules will follow these core principles:
1. Default Deny
- All access denied by default (
.read: false,.write: false) - Explicit permissions must be granted for each node
- No global read/write access
2. Company Isolation
- Users can only access data from their own company
- Enforced through
companyIdfield checks - No cross-company data leakage
3. User Isolation
- Users can only access their own personal data
- Profile information protected from other users
- Privacy by default
4. Role-Based Access Control (RBAC)
- Employee: Read own data, read company data, limited write
- Manager: Read/write for managed positions
- Admin: Full read/write within own company
- SuperAdmin: Cross-company access for head office
5. Least Privilege
- Grant minimum necessary permissions
- Specific permissions for specific operations
- No blanket access grants
Data Model Requirements
The current data model already supports proper security (verified via code analysis):
Required Fields Present
Users Node (/Users/{userId}):
- ✅
isHeadOffice- Super admin flag for multi-company access - ✅
headOfficeAccess- Object mapping companyIds to access grants
Employees Node (/Employees/{employeeId}):
- ✅
companyId- Which company this employee belongs to - ✅
userId- Which user this employee record represents - ✅
isAdmin- Company admin privileges
Companies Node (/Companies/{companyId}):
- ✅
createdBy- User who created the company
Path-Scoped Nodes:
- ✅ Chats:
/Chats/{companyId}/{chatId}- Company in path - ✅ Notifications:
/Notifications/{companyId}/{...}- Company in path - ✅ Schedules:
/WeeklySchedule/{companyId}/{...}- Company in path
Conclusion: No data model changes required. All necessary fields exist.
Proposed Security Rules Structure
Root Level - Default Deny
{
"rules": {
".read": false,
".write": false,
// ... specific node rules below
}
}
Users Node - Self Access Only
"Users": {
"$userId": {
".read": "auth != null && auth.uid === $userId",
".write": "auth != null && auth.uid === $userId",
".validate": "newData.hasChildren(['email', 'name'])"
}
}
Access Pattern:
- Users can read/write ONLY their own user record
- No cross-user access
- Basic validation to ensure required fields
Employees Node - Company Scoped
"Employees": {
"$employeeId": {
".read": "auth != null && (
data.child('userId').val() === auth.uid ||
root.child('Employees').orderByChild('userId').equalTo(auth.uid).orderByChild('companyId').equalTo(data.child('companyId').val()).once('value').exists()
)",
".write": "auth != null && (
(data.child('userId').val() === auth.uid && root.child('Employees').orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === false) ||
(root.child('Employees').orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === true &&
root.child('Employees').orderByChild('userId').equalTo(auth.uid).child('companyId').val() === data.child('companyId').val())
)",
".indexOn": ["companyId", "userId", "customEmplNumber"]
}
}
Access Pattern:
- Employees can read their own record
- Employees can read other employees in the same company
- Regular employees can update limited fields in their own record
- Company admins can read/write all employees in their company
- No cross-company access
Companies Node - Company Members Only
"Companies": {
"$companyId": {
".read": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).once('value').exists() ||
root.child('Users').child(auth.uid).child('isHeadOffice').val() === true
)",
".write": "auth != null && (
(root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === true) ||
(data.child('createdBy').val() === auth.uid) ||
(root.child('Users').child(auth.uid).child('isHeadOffice').val() === true)
)",
".indexOn": ["createdBy", "notifyBefor", "key", "hasAccessToAttendance"]
}
}
Access Pattern:
- Only employees of a company can read company data
- Only company admins can modify company settings
- Company creator has special write access
- Head office users can read/write all companies
Company Access Codes - Secure Implementation
Option A: Scoped Access (Keep Current System)
"Passwords": {
"$companyId": {
".read": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).once('value').exists() ||
root.child('Users').child(auth.uid).child('isHeadOffice').val() === true
)",
".write": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === true
)"
}
}
Access Pattern:
- Only company members can read their company's access code
- Only company admins can update the access code
- No cross-company code access
Option B: Remove Entirely (Better Long-Term)
Replace with Firebase Dynamic Links or email invitations:
- Generate unique invitation links per employee
- Include company context in the link
- Automatic expiration after 7 days
- Track who invited whom
Messages & Chats - Participant Access Only
Chats Node (already path-scoped):
"Chats": {
"$companyId": {
"$chatId": {
".read": "auth != null && (
data.child('members').child(auth.uid).val() === true ||
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === true
)",
".write": "auth != null && (
!data.exists() ||
data.child('members').child(auth.uid).val() === true
)",
"members": {
"$uid": {
".indexOn": ".value"
}
}
}
}
}
Access Pattern:
- Only chat participants can read messages
- Company admins can read all chats (for compliance)
- Only participants can write to existing chats
Messages Node:
"Messages": {
"$messageId": {
".read": "auth != null && (
data.child('senderId').val() === auth.uid ||
root.child('Chats').child(data.child('dialogId').val().split('/')[0]).child(data.child('dialogId').val().split('/')[1]).child('members').child(auth.uid).val() === true
)",
".write": "auth != null && data.child('senderId').val() === auth.uid",
".indexOn": ["dialogId"]
}
}
Access Pattern:
- Only sender and chat participants can read messages
- Only sender can write (create) messages
- Messages linked to chats via dialogId
Schedules - Company Scoped
"WeeklySchedule": {
"$companyId": {
"$date": {
"$employeeId": {
".read": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).once('value').exists()
)",
".write": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).child('isAdmin').val() === true
)"
}
}
}
}
Access Pattern:
- All company employees can read schedules
- Only company admins can write/modify schedules
- Path-scoped by companyId for efficient queries
Notifications - Company Scoped
"NotificationsFlatV2": {
"$companyId": {
"$employeeId": {
".read": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).once('value').exists()
)",
".write": "auth != null && (
root.child('Employees').orderByChild('companyId').equalTo($companyId).orderByChild('userId').equalTo(auth.uid).once('value').exists()
)",
"$state": {
"$type": {
".indexOn": ["createdAt"]
}
}
}
}
}
Access Pattern:
- Employees can read/write notifications in their company
- Filtering by employee and type for efficient queries
- Path-scoped by companyId and employeeId
Public Nodes
Version Information:
"VersionWeb": {
".read": true,
".write": "auth != null && root.child('Users').child(auth.uid).child('isHeadOffice').val() === true"
},
"VersionMobile": {
".read": true,
".write": false
}
Support:
"Support": {
".read": "auth != null && root.child('Users').child(auth.uid).child('isHeadOffice').val() === true",
".write": "auth != null"
}
Implementation Plan
Phase 1: Preparation (Day 1)
1.1 Code Review
- Review all code that reads/writes to Firebase
- Identify queries that may break with new rules
- Document all access patterns
1.2 Backup Current Rules
cp database.rules.json database.rules.json.backup.$(date +%Y%m%d)
1.3 Create New Rules File
- Implement all security rules in new file
- Include all indexes
- Add comments for maintainability
Phase 2: Testing (Days 2-3)
2.1 Write Automated Tests
Create tests/security-rules.test.js:
const firebase = require('@firebase/rules-unit-testing');
describe('Firebase Security Rules', () => {
// Test Users node
test('User can read own data', async () => {
const db = getDb({ uid: 'user1' });
await firebase.assertSucceeds(
db.ref('Users/user1').once('value')
);
});
test('User cannot read other user data', async () => {
const db = getDb({ uid: 'user1' });
await firebase.assertFails(
db.ref('Users/user2').once('value')
);
});
// Test Employees node
test('Employee can read company employees', async () => {
// Set up mock data
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1'
});
await admin.ref('Employees/emp2').set({
userId: 'user2',
companyId: 'company1'
});
const db = getDb({ uid: 'user1' });
await firebase.assertSucceeds(
db.ref('Employees/emp2').once('value')
);
});
test('Employee cannot read other company employees', async () => {
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1'
});
await admin.ref('Employees/emp2').set({
userId: 'user2',
companyId: 'company2'
});
const db = getDb({ uid: 'user1' });
await firebase.assertFails(
db.ref('Employees/emp2').once('value')
);
});
// Test Companies node
test('Admin can write to own company', async () => {
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1',
isAdmin: true
});
const db = getDb({ uid: 'user1' });
await firebase.assertSucceeds(
db.ref('Companies/company1').update({ name: 'New Name' })
);
});
test('Employee cannot write to company', async () => {
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1',
isAdmin: false
});
const db = getDb({ uid: 'user1' });
await firebase.assertFails(
db.ref('Companies/company1').update({ name: 'New Name' })
);
});
// Test Company Codes
test('Employee can read own company code', async () => {
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1'
});
await admin.ref('Passwords/company1').set({
password: 'testcode'
});
const db = getDb({ uid: 'user1' });
await firebase.assertSucceeds(
db.ref('Passwords/company1').once('value')
);
});
test('Employee cannot read other company codes', async () => {
const admin = getAdminDb();
await admin.ref('Employees/emp1').set({
userId: 'user1',
companyId: 'company1'
});
await admin.ref('Passwords/company2').set({
password: 'testcode'
});
const db = getDb({ uid: 'user1' });
await firebase.assertFails(
db.ref('Passwords/company2').once('value')
);
});
});
2.2 Manual Test Scenarios
Create test checklist in staging:
- Employee A logs in and views their profile
- Employee A views their company information
- Employee A views other employees in same company
- Employee A CANNOT view employees in other companies
- Employee A CANNOT modify company settings
- Admin B logs in and modifies company settings
- Admin B modifies employee records in their company
- Admin B CANNOT modify other companies
- Employee C sends a message in a chat
- Employee C reads messages in their chats
- Employee C CANNOT read messages in other chats
- Admin D views schedules for their company
- Admin D modifies schedules
- Employee E views their schedule
- Employee E CANNOT modify schedules
- All queries still work with proper indexes
- Application performance is acceptable
- No broken features
Phase 3: Staging Deployment (Day 4)
3.1 Pre-Deployment
# Ensure correct environment
firebase use default # Should be pivot-not-production-project
# Verify account
gcloud config get-value account # Should be ciprian@pivotapp.ca
# Backup current staging rules
firebase database:get /.settings/rules > staging-rules-backup-$(date +%Y%m%d).json
# Dry run (if available)
firebase deploy --only database --dry-run
3.2 Deployment
# Deploy new rules
firebase deploy --only database
# Expected output:
# ✓ database: rules ready to deploy.
# ✓ Deploy complete!
3.3 Post-Deployment Monitoring
# Monitor Firebase Console
# - Check for denied operations in Rules Usage
# - Review logs for errors
# - Monitor query performance
# Check application logs
# - Look for permission denied errors
# - Track user reports
# - Monitor error rates
3.4 Validation
- Run full automated test suite
- Run manual test scenarios
- Monitor for 24-48 hours
- Gather feedback from team testing
- Fix any issues discovered
Phase 4: Production Deployment (Day 5-7)
⚠️ REQUIRES EXPLICIT APPROVAL FROM CHIP
4.1 Pre-Production Checklist
- All staging tests passed
- No issues reported in 48-hour monitoring period
- Explicit approval received from Chip
- Production backup created
- Rollback plan documented and ready
- Team notified of deployment window
- Monitoring dashboard ready
4.2 Production Backup
# Switch to production (with permission)
firebase use production
# Verify project
firebase projects:list # Should show pivot-inc
# Backup entire database (if small enough)
firebase database:get / > production-backup-$(date +%Y%m%d_%H%M%S).json
# Backup current rules
firebase database:get /.settings/rules > production-rules-backup-$(date +%Y%m%d_%H%M%S).json
# Keep backup locally and in secure storage
4.3 Production Deployment
# Final verification
firebase use production
gcloud config get-value account # Should be ciprian@pivotapp.ca
gcloud config get-value project # Should be pivot-inc
# Deploy with caution
firebase deploy --only database
# Monitor immediately after deployment
4.4 Post-Production Monitoring
First Hour:
- Watch Firebase Console Real-Time Dashboard
- Monitor denied operations
- Check error logs
- Be ready for immediate rollback
First 24 Hours:
- Continuous monitoring of errors
- Track user reports
- Monitor support channels
- Check query performance
First Week:
- Daily monitoring
- Gather user feedback
- Review security analytics
- Ensure no regressions
Phase 5: Post-Deployment (Ongoing)
5.1 Security Monitoring
Enable Firebase Security Rules analytics:
- Track denied operations
- Monitor unusual access patterns
- Set up alerts for:
- High rate of denied reads
- High rate of denied writes
- Bulk data access attempts
- Failed authentication attempts
5.2 Documentation
- Update developer documentation
- Document new security model
- Create security best practices guide
- Update API documentation
- Add security rules to code review checklist
5.3 Security Audit
Schedule regular security reviews:
- Quarterly security rules review
- Annual penetration testing
- Code review for all security changes
- Automated security testing in CI/CD
Rollback Plan
If issues occur after deployment:
Immediate Rollback (< 5 minutes)
# Restore old rules from backup
cp database.rules.json.backup.YYYYMMDD database.rules.json
# Deploy old rules
firebase deploy --only database
# Verify rollback
# Check Firebase Console
# Test basic operations
# Monitor error rates
Post-Rollback
-
Investigate Issues
- Collect error logs
- Identify broken queries
- Document specific failures
-
Fix Rules
- Address specific issues
- Update tests
- Re-test in staging
-
Retry Deployment
- Follow same deployment process
- Monitor more closely
- Have faster rollback ready
Success Metrics
Security Metrics
- ✅ Zero cross-company data access
- ✅ All queries properly scoped by company
- ✅ Company access codes only visible to company members
- ✅ User data only accessible by the user themselves
- ✅ Proper admin privilege enforcement
Application Metrics
- ✅ Zero broken features
- ✅ Query performance within acceptable range (< 10% degradation)
- ✅ No increase in error rates
- ✅ Positive user feedback
- ✅ All automated tests passing
Process Metrics
- ✅ Staged rollout completed successfully
- ✅ No emergency rollbacks required
- ✅ Documentation completed
- ✅ Team trained on new security model
- ✅ Monitoring and alerting in place
Long-Term Improvements
Phase 6: Enhanced Security (Months 2-3)
6.1 Migrate from Company Codes to Invitations
- Implement Firebase Dynamic Links
- Email-based invitation system
- Automatic expiration (7 days)
- Track invitation senders
- Revoke unused invitations
6.2 Implement Audit Logging
- Log all sensitive data access
- Track who accessed what and when
- Store logs in separate, append-only location
- Retention policy (90 days)
- Automated anomaly detection
6.3 Add Client-Side Encryption
- Encrypt sensitive fields before storage
- Use Firebase App Check
- Implement Cloud Functions for sensitive operations
- Key management strategy
Phase 7: Platform Migration (Months 4-6)
7.1 Consider Firestore Migration
Advantages of Firestore over Realtime Database:
- More powerful security rules
- Better querying with security
- Easier to implement company isolation
- Better offline support
- More scalable
7.2 Migration Plan
- Create Firestore data model
- Implement dual-write period
- Migrate data incrementally
- Update client applications
- Deprecate Realtime Database
Risk Assessment
Implementation Risks
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Rules too restrictive, lock out users | Medium | High | Thorough testing, staged rollout |
| Performance degradation | Low | Medium | Load testing, query optimization |
| Broken features | Medium | High | Comprehensive test coverage |
| Production deployment issues | Low | Critical | Extensive staging testing, quick rollback |
| User complaints during transition | Low | Low | Clear communication, support readiness |
Benefits vs. Risks
Benefits:
- Eliminate catastrophic security vulnerability
- Comply with data privacy regulations
- Protect customer data
- Reduce legal liability
- Improve customer trust
Risks:
- Temporary service disruption (if rules incorrect)
- Development time investment
- Potential for bugs during transition
Conclusion: Benefits far outweigh risks, especially with proper testing and staged rollout.
Questions & Decisions Needed
Before implementation, the following decisions are needed from Chip:
-
Company Access Codes
- Option A: Keep with proper security (faster)
- Option B: Migrate to email invitations (better long-term)
- Recommendation: Option A now, Option B in 3-6 months
-
Timeline
- Start implementation immediately?
- Schedule production deployment window?
- Recommendation: Start staging work immediately
-
Testing Resources
- Test accounts available?
- Team members for UAT?
- Recommendation: Need 2-3 test accounts in staging
-
Monitoring
- Who monitors post-deployment?
- Escalation path for issues?
- Recommendation: DevOps on-call during deployment
Conclusion
The proposed solution addresses all identified security vulnerabilities while maintaining application functionality. The staged rollout approach minimizes risk, and the comprehensive testing ensures no regressions.
Key Takeaways:
- All required data model fields already exist
- No application code changes required (only rules)
- Staged rollout minimizes production risk
- Comprehensive testing ensures quality
- Clear rollback plan for emergencies
- Long-term improvements roadmap
Next Step: Await approval from Chip to proceed with implementation.
References
- Problem Statement: Firebase Security Vulnerabilities
- Current Rules:
/home/chipdev/pivot-meta/pivot/database.rules.json - Data Model Analysis:
/tmp/firebase-database-structure-analysis.md - Firebase Security Rules: https://firebase.google.com/docs/database/security
- Firebase Testing: https://firebase.google.com/docs/rules/unit-tests