Cache Poisoning in Firestore
How Cache Poisoning Manifests in Firestore
Cache poisoning in Firestore occurs when attackers manipulate cached data to serve malicious or outdated content to legitimate users. Unlike traditional web caches, Firestore's client-side caching and synchronization mechanisms create unique attack vectors that developers must understand.
The most common Firestore cache poisoning scenario involves race conditions during document updates. When multiple clients read and write the same document simultaneously, Firestore's cache can serve stale data that bypasses security rules. Consider this vulnerable pattern:
const docRef = db.collection('users').doc(userId);
const doc = await docRef.get();
const data = doc.data();
// Business logic here
await docRef.set({ balance: data.balance - amount });An attacker can exploit this by rapidly requesting the document before the cache updates, causing the client to operate on outdated balance information. This leads to overdraft attacks where users can withdraw more than their actual balance.
Another manifestation involves cache persistence across user sessions. Firestore's offline persistence stores data locally, and if an attacker gains access to a device, they can modify cached documents. When the user reconnects, these poisoned documents sync to the server, potentially bypassing validation rules:
// Vulnerable: no cache invalidation
const docRef = db.collection('posts').doc(postId);
const doc = await docRef.get();
if (doc.exists) {
// Process cached data without revalidation
processPost(doc.data());
}Collection-level caching creates additional risks. When developers cache entire collections without proper filtering, attackers can manipulate cache keys or query parameters to access unauthorized documents. This is particularly dangerous in admin interfaces where cached results might show sensitive user data.
Firestore's real-time listeners compound cache poisoning risks. When a listener is established with incorrect query parameters or security rules, the cached stream can deliver unauthorized data to clients. The vulnerability often lies in how query cursors are cached and reused across different user contexts.
Firestore-Specific Detection
Detecting cache poisoning in Firestore requires a multi-layered approach combining static analysis, runtime monitoring, and automated scanning. The first step is identifying vulnerable code patterns through static analysis tools that understand Firestore's specific APIs.
middleBrick's Firestore-specific scanning engine examines your application for common cache poisoning indicators. The scanner analyzes your Firestore SDK usage patterns, looking for:
- Unprotected document reads that cache data without revalidation
- Missing transaction boundaries around cached operations
- Insecure query parameter handling that could lead to cache key manipulation
- Real-time listener configurations without proper access controls
- Offline persistence usage without cache invalidation strategies
- Collection reads without proper filtering or pagination
The scanner also performs active testing by simulating concurrent access patterns and race conditions. It attempts to read documents during write operations to detect stale cache serving. For real-time applications, middleBrick tests whether listeners can be manipulated to deliver unauthorized data streams.
Runtime monitoring complements automated scanning by tracking cache hit rates and data consistency. Tools like Firebase Performance Monitoring can reveal when cached data is served instead of fresh data, helping identify potential poisoning scenarios. Look for:
- Unexpected cache hit rates on sensitive documents
- Data inconsistencies between client and server states
- Unusual access patterns suggesting cache manipulation
- Offline-to-online sync anomalies
For comprehensive detection, implement custom logging around cache operations. Track when documents are served from cache versus fetched fresh, and log any cache misses on critical operations. This creates an audit trail for investigating potential poisoning incidents.
Firestore-Specific Remediation
Remediating cache poisoning in Firestore requires implementing defense-in-depth strategies that address both client-side and server-side vulnerabilities. The foundation is proper transaction usage for all operations that read-modify-write data:
// Secure pattern using transactions
const docRef = db.collection('users').doc(userId);
await db.runTransaction(async (transaction) => {
const doc = await transaction.get(docRef);
const data = doc.data();
// Business logic with atomic operations
const newBalance = data.balance - amount;
if (newBalance < 0) {
throw new Error('Insufficient funds');
}
transaction.set(docRef, { balance: newBalance });
});Transactions automatically handle cache invalidation and ensure all clients see consistent data. They prevent the race conditions that enable cache poisoning by serializing conflicting operations.
Implement cache control headers and time-to-live (TTL) strategies for cached data. Firestore doesn't support TTL natively, so use Cloud Functions to implement automatic document expiration:
// Cloud Function to invalidate stale cache
exports.invalidateStaleCache = functions.pubsub.schedule('every 5 minutes')
.onRun(async (context) => {
const cutoff = admin.firestore.Timestamp.now().toDate();
const cutoffTimestamp = admin.firestore.Timestamp.fromDate(cutoff);
const staleDocs = await admin.firestore()
.collection('sensitive-data')
.where('cacheTimestamp', '<', cutoffTimestamp)
.get();
const batch = admin.firestore().batch();
staleDocs.forEach(doc => {
batch.delete(doc.ref);
});
await batch.commit();
});For real-time applications, implement query-based caching with proper security rules. Use Firestore's query capabilities to limit cached data scope:
// Secure query with proper filtering
const userId = getAuthUserId();
const query = db.collection('posts')
.where('authorId', '==', userId)
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(20);
const docs = await query.get();
// Cache only the filtered results
cache.set(`user_posts_${userId}`, docs.docs.map(d => d.data()));Implement offline persistence controls to prevent cache poisoning across sessions. Use Firestore's persistence settings to control what data persists offline:
// Configure offline persistence with security
const config = {
persistence: true,
cacheSizeBytes: 5 * 1024 * 1024, // Limit cache size
cacheControl: {
maxAge: 300, // 5 minutes
mustRevalidate: true
}
};
const app = firebase.initializeApp(config);
Finally, implement comprehensive security rules that validate data integrity regardless of cache state. Rules should check for logical consistency and prevent impossible states:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if
request.auth.uid == userId &&
(request.resource.data.balance <= resource.data.balance ||
request.time < resource.data.lastTransaction);
}
}
}