Insecure Deserialization in Firestore
How Insecure Deserialization Manifests in Firestore
Insecure deserialization in Firestore occurs when applications trust serialized data from untrusted sources without proper validation. Unlike traditional web applications where deserialization vulnerabilities often involve Java serialization or PHP object injection, Firestore presents unique attack vectors through its document data model and client SDK behaviors.
The most common Firestore deserialization vulnerability appears when applications deserialize Firestore document data directly into application objects or classes. Consider this vulnerable pattern:
const doc = await db.collection('users').doc(userId).get();
const userData = doc.data();
// Vulnerable: directly using untrusted document data
const user = new User(userData);
The attacker can manipulate document fields to trigger unexpected behavior during object construction. If the User class has a constructor that processes certain fields in privileged ways, an attacker can craft document data to execute arbitrary code or bypass authorization checks.
Firestore's FieldValue objects present another deserialization vector. When documents contain FieldValue.delete() markers or other special field values, improper handling can lead to unexpected behavior:
const doc = await db.collection('config').doc('appSettings').get();
const settings = doc.data();
// If settings contains FieldValue.delete(), this may cause issues
applySettings(settings);
Cloud Functions triggered by Firestore writes can also be vulnerable to deserialization attacks. When a function receives data from a document write, it must validate that the data structure matches expectations:
exports.onUserUpdate = functions.firestore
.document('users/{userId}')
.onWrite((change, context) => {
const newData = change.after.data();
// Vulnerable: no validation of newData structure
processUserData(newData);
});
The onWrite trigger receives the entire document data, and if the processing function doesn't validate the structure, an attacker can craft documents with malicious field values that trigger code execution during processing.
Firestore's ArrayUnion, ArrayRemove, and Increment field values can also be exploited if the application logic assumes these are simple numeric values:
const doc = await db.collection('inventory').doc(itemId).get();
const data = doc.data();
const quantity = data.quantity; // Could be FieldValue.Increment()
// Vulnerable: treating FieldValue as a number
const total = quantity * price;
This type confusion can lead to calculation errors, authorization bypasses, or unexpected application behavior.
Firestore-Specific Detection
Detecting insecure deserialization in Firestore requires examining both the data layer and application code. middleBrick's Firestore-specific scanning identifies these vulnerabilities through several techniques:
Document Structure Analysis: The scanner examines document schemas to identify fields that could contain serialized data or special FieldValue objects. It flags documents where field types don't match expected schemas or where complex nested structures could hide malicious payloads.
Client SDK Usage Patterns: middleBrick analyzes code patterns that commonly lead to deserialization vulnerabilities:
// Vulnerable pattern detected
const userData = await db.collection('users').doc(id).get();
const user = new User(userData.data()); // No validation
The scanner flags direct object construction from document data without validation as a high-risk pattern.
Cloud Function Trigger Analysis: For Cloud Functions triggered by Firestore events, middleBrick examines the data flow from document writes to function processing:
exports.processOrder = functions.firestore
.document('orders/{orderId}')
.onCreate((snap) => {
const orderData = snap.data();
// Scanner flags: no validation of orderData structure
validateOrder(orderData);
});
The tool identifies missing validation layers between document data and business logic processing.
FieldValue Object Handling: middleBrick specifically checks for proper handling of Firestore's special field values:
// Detected vulnerability
const doc = await db.collection('settings').doc('app').get();
const settings = doc.data();
// Scanner flags: potential FieldValue.delete() handling
applySettings(settings);
The scanner recommends explicit checks for FieldValue instances before processing document data.
Security Rule Analysis: middleBrick also examines Firestore Security Rules to ensure they don't inadvertently allow deserialization attacks through overly permissive write rules:
// Vulnerable Security Rules detected
match /users/{userId} {
allow write: if true; // Too permissive
}
The scanner recommends principle of least privilege for document write permissions.
Firestore-Specific Remediation
Remediating insecure deserialization in Firestore requires a defense-in-depth approach combining data validation, type checking, and secure coding patterns. Here are Firestore-specific remediation strategies:
Schema Validation with Type Guards: Always validate document data against expected schemas before processing:
interface User {
id: string;
email: string;
name: string;
role: 'user' | 'admin' | 'moderator';
}
function validateUser(data: any): data is User {
const required = ['id', 'email', 'name', 'role'];
if (!required.every((field) => field in data)) {
return false;
}
if (typeof data.id !== 'string' ||
typeof data.email !== 'string' ||
typeof data.name !== 'string' ||
!['user', 'admin', 'moderator'].includes(data.role)) {
return false;
}
return true;
}
// Secure usage
const doc = await db.collection('users').doc(userId).get();
const data = doc.data();
if (data && validateUser(data)) {
const user = new User(data); // Safe: validated first
} else {
throw new Error('Invalid user data structure');
}
FieldValue Type Checking: Explicitly check for Firestore's special field values:
import { FieldValue } from '@google-cloud/firestore';
function isFieldValue(value: any): value is FieldValue {
return value instanceof FieldValue;
}
function safeProcessData(data: any): void {
Object.keys(data).forEach((key) => {
if (isFieldValue(data[key])) {
console.warn(`Skipping special FieldValue at ${key}`);
delete data[key];
}
});
// Now safe to process
processData(data);
}
Cloud Function Input Validation: For Cloud Functions triggered by Firestore writes, implement strict input validation:
exports.onUserCreate = functions.firestore
.document('users/{userId}')
.onCreate((snap) => {
const data = snap.data();
// Validate structure first
if (!validateUserData(data)) {
console.error('Invalid user data:', data);
throw new functions.https.HttpsError(
'invalid-argument',
'User data validation failed'
);
}
// Safe to process
return createUserRecord(data);
});
Secure Object Construction: Use factory patterns or builders that validate input before object creation:
class User {
private constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string,
public readonly role: UserRole
) {}
static fromFirestoreData(data: any): User | null {
if (!validateUser(data)) {
return null;
}
return new User(
data.id,
data.email,
data.name,
data.role
);
}
}
Security Rules Enhancement: Implement principle of least privilege in Security Rules:
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if validateUserData(request.resource.data) &&
isAuthenticated() &&
request.auth.uid == userId;
allow update: if validateUserData(request.resource.data) &&
isAuthenticated() &&
request.auth.uid == userId;
allow delete: if isAuthenticated() && request.auth.uid == userId;
}
These remediation patterns eliminate deserialization vulnerabilities by ensuring all data from Firestore is validated and sanitized before use in application logic.