Integrity Failures in Firestore
How Integrity Failures Manifests in Firestore
Integrity failures in Firestore occur when attackers manipulate data through unauthorized modifications, bypassing business logic controls. These failures typically manifest through four primary attack vectors specific to Firestore's architecture.
The most common pattern involves client-side data manipulation. Since Firestore allows direct client access to documents and collections, malicious users can modify request payloads before they reach the database. An attacker might intercept a legitimate request to update their user profile, then modify the JSON payload to escalate privileges or alter unrelated user data:
const updateProfile = async (userId, profileData) => {
const userDoc = db.collection('users').doc(userId);
await userDoc.update(profileData);
// Integrity failure: client can modify any field
// Attacker could add: { role: 'admin', balance: 9999 }
}Another critical manifestation is collection traversal attacks. Firestore's hierarchical structure allows navigation from one collection to another if path traversal isn't properly validated. An attacker might exploit this to access documents outside their authorized scope:
// Vulnerable: No validation of collection path
const getDocument = async (collectionPath, docId) => {
const docRef = db.doc(`${collectionPath}/${docId}`);
return await docRef.get();
}
// Attacker could call: getDocument('users/../admin', 'config')
// This might access: /admin/config instead of /users/configBatch operation abuse represents another integrity failure vector. Firestore's batch writes allow multiple operations in a single transaction, but if the batch includes operations the user shouldn't perform, integrity is compromised:
// Vulnerable batch operation
const processOrder = async (userId, orderId) => {
const batch = db.batch();
// User should only update their own order
batch.update(db.collection('orders').doc(orderId), { status: 'processing' });
// Attacker adds: batch.update(db.collection('inventory').doc('all'), { stock: 0 })
await batch.commit();
}Finally, security rule bypasses through complex document structures can lead to integrity failures. When documents contain arrays or nested objects that aren't properly validated, attackers can inject malicious data structures:
// Vulnerable: No validation of array contents
const addPermission = async (userId, permission) => {
const userDoc = db.collection('users').doc(userId);
await userDoc.update({
permissions: admin.firestore.FieldValue.arrayUnion(permission)
});
// Attacker could add: { admin: true, deleteAll: true }
}Firestore-Specific Detection
Detecting integrity failures in Firestore requires examining both your security rules and application code. Start by analyzing your Firestore security rules for overly permissive patterns that could allow unauthorized data manipulation.
Security Rule Analysis should focus on identifying rules that grant excessive write permissions. Look for patterns like:
match /databases/{database}/documents {
match /users/{userId}/orders/{orderId} {
// Vulnerable: allows any authenticated user to write any order
allow write: if request.auth != null;
}
match /{document=**} {
// Vulnerable: allows any authenticated user to write anywhere
allow write: if request.auth != null;
}
}middleBrick's Firestore scanner specifically tests for these integrity failure patterns by attempting to modify data across different user contexts. The scanner simulates authenticated users with varying privilege levels to identify where data can be manipulated beyond authorized boundaries.
Code Review Patterns should examine how your application validates and processes Firestore operations. Key areas to investigate:
// What to look for:
- Direct client writes without server validation
- Missing field-level authorization checks
- Unvalidated batch operations
- Insecure collection path construction
// middleBrick scans for these patterns automatically:
- Attempts to write to collections the user shouldn't access
- Modification of document fields outside the intended scope
- BOLA (Broken Object Level Authorization) vulnerabilities
- Privilege escalation through data manipulationRuntime Detection involves monitoring for suspicious patterns in your Firestore operations. Look for:
- Unexpected field modifications in audit logs
- Batch operations containing unauthorized document references
- Collection path traversal attempts
- Mass data modifications from single requests
The middleBrick CLI tool can scan your Firestore endpoints and provide detailed reports on integrity failure risks:
npx middlebrick scan https://firestore.googleapis.com/
# Output includes:
# - Integrity failure risk score
# - Specific vulnerable endpoints
# - Severity levels for each finding
# - Remediation guidance for Firestore-specific issuesFirestore-Specific Remediation
Remediating integrity failures in Firestore requires a defense-in-depth approach combining security rules, server-side validation, and proper data modeling. The most effective strategy starts with granular security rules that enforce field-level authorization:
match /databases/{database}/documents {
match /users/{userId}/orders/{orderId} {
// Only allow users to modify their own orders, and only specific fields
allow read, update: if request.auth.uid == userId &&
isValidOrderUpdate(request.resource.data);
allow create: if request.auth.uid == userId &&
isValidNewOrder(request.resource.data);
allow delete: if request.auth.uid == userId &&
isOrderDeletable(request.time, request.resource.data);
}
// Helper functions for field-level validation
function isValidOrderUpdate(data) {
return data.keys().hasOnly(['status', 'shippingAddress', 'items']) &&
data.isString('status') &&
data.isBoolean('isShipped') == false; // Prevent status override
}
}Server-side validation provides an additional layer of protection by validating all data before it reaches Firestore. Use Cloud Functions or your backend to enforce business logic:
const admin = require('firebase-admin');
const db = admin.firestore();
// Server-side validation middleware
const validateAndUpdateUser = async (userId, updateData) => {
const userDoc = db.collection('users').doc(userId);
const userSnap = await userDoc.get();
if (!userSnap.exists) {
throw new Error('User not found');
}
const userData = userSnap.data();
// Validate each field against business rules
const validatedData = validateUserData(updateData, userData);
// Only allow specific fields to be updated
const allowedFields = ['email', 'displayName', 'preferences'];
const filteredData = Object.fromEntries(
Object.entries(validatedData).filter(([key]) => allowedFields.includes(key))
);
await userDoc.update(filteredData);
return filteredData;
};
function validateUserData(updateData, existingData) {
const result = {};
// Email validation
if (updateData.email) {
if (!isValidEmail(updateData.email)) {
throw new Error('Invalid email format');
}
result.email = updateData.email;
}
// Role escalation prevention
if (updateData.role && updateData.role !== existingData.role) {
throw new Error('Role modification not allowed');
}
// Other field validations...
return result;
}Transaction-based operations ensure atomicity and prevent partial updates that could lead to inconsistent state:
const processPaymentTransaction = async (orderId, paymentData) => {
const transaction = db.transaction();
return transaction.get(db.collection('orders').doc(orderId))
.then(orderSnap => {
const orderData = orderSnap.data();
// Validate payment amount matches order total
if (paymentData.amount !== orderData.total) {
throw new Error('Payment amount mismatch');
}
// Check order status
if (orderData.status !== 'pending') {
throw new Error('Order already processed');
}
// Update order and create payment record atomically
transaction.update(orderSnap.ref, { status: 'paid' });
transaction.set(db.collection('payments').doc(), {
orderId: orderId,
amount: paymentData.amount,
method: paymentData.method,
timestamp: admin.firestore.Timestamp.now()
});
})
.then(() => transaction.commit())
.catch(error => {
console.error('Transaction failed:', error);
throw error;
});
};Audit logging helps detect and respond to integrity failures by tracking all data modifications:
const logDataChange = async (userId, documentPath, oldData, newData, operation) => {
const auditLog = db.collection('audit_logs').doc();
await auditLog.set({
userId: userId,
documentPath: documentPath,
oldData: oldData,
newData: newData,
operation: operation,
timestamp: admin.firestore.Timestamp.now(),
ipAddress: getClientIP(), // Implement your own IP detection
userAgent: getClientUserAgent()
});
// Alert on suspicious patterns
if (shouldAlert(operation, oldData, newData)) {
await sendAlertEmail('Integrity alert', generateAlertMessage(...));
}
};
function shouldAlert(operation, oldData, newData) {
// Alert on privilege escalation attempts
if (operation === 'update' &&
newData.role &&
newData.role !== oldData.role &&
newData.role === 'admin') {
return true;
}
// Alert on mass data modifications
if (operation === 'update' &&
Object.keys(newData).length > 10) {
return true;
}
return false;
}