Command Injection in Firestore
How Command Injection Manifests in Firestore
Command injection in Firestore contexts typically occurs when user input is improperly incorporated into database queries or security rules without proper sanitization. Unlike traditional command injection in operating systems, Firestore injection attacks exploit the database's query language and security model.
The most common pattern involves dynamic query construction where user input directly influences Firestore's query structure. For example, consider an application that allows users to search for documents by field:
// Vulnerable code - direct string interpolation
const field = req.query.field;
const value = req.query.value;
const query = db.collection('users').where(field, '==', value);
const results = await query.get();This code is vulnerable because an attacker can manipulate the field parameter to inject arbitrary query conditions. An attacker could supply field=role and value=admin to retrieve administrative users, or craft more complex queries that bypass intended security constraints.
Another manifestation occurs in Firestore security rules where dynamic evaluation happens. Consider these vulnerable rules:
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
}While this looks secure, if the application layer constructs queries using user input for the userId parameter, an attacker could potentially enumerate users or access unauthorized documents by manipulating the path parameter.
Firestore's array operations can also be exploited. A vulnerable pattern might look like:
// Vulnerable - no input validation
const updateData = {
[req.body.field]: admin.firestore.FieldValue.arrayUnion(req.body.value)
};
await db.collection('users').doc(userId).update(updateData);Here, an attacker could inject malicious field names or values that manipulate the array operations in unintended ways.
Batch operations present another injection vector. Consider:
const batch = db.batch();
req.body.updates.forEach(update => {
batch.update(db.collection('users').doc(update.id), update.changes);
});
await batch.commit();If update.id or update.changes contains malicious data, an attacker could modify documents they shouldn't have access to.
Firestore-Specific Detection
Detecting command injection in Firestore requires examining both the application code and the runtime behavior of queries. Here are the specific techniques and tools for identifying these vulnerabilities:
Static Code Analysis should look for patterns where user input directly influences Firestore operations. Key indicators include:
- Dynamic field names constructed from request parameters
- Unvalidated path parameters used in document references
- Array operations with user-controlled field names
- Batch operations processing unvalidated input
- Security rules that rely on application-layer validation
Runtime Detection involves monitoring query patterns and access attempts. Look for:
- Queries with unexpected field names or operators
- Access attempts to system collections (like
_firestore) - Unusual array operations or field manipulations
- Batch operations affecting multiple documents outside normal patterns
middleBrick Scanning specifically tests for Firestore command injection vulnerabilities through black-box scanning. The tool automatically:
- Tests for path traversal in document references
- Attempts to inject malicious field names and operators
- Scans for improper array operation handling
- Checks batch operation security
- Analyzes security rule effectiveness against injection attempts
The scanner returns a security score (A-F) with specific findings about Firestore injection vulnerabilities, prioritized by severity with remediation guidance.
Monitoring and Logging should track:
// Enhanced logging for detection
const logQuery = (query, userId) => {
console.log(`Query by ${userId}: ${query._query.toString()}`);
// Log suspicious patterns
if (query._query._fields.some(f => f.includes('$') || f.includes('..'))) {
console.warn('Suspicious query detected:', query._query._fields);
}
};Production monitoring should alert on queries with unusual field names, unexpected operators, or access patterns that deviate from normal user behavior.
Firestore-Specific Remediation
Remediating command injection in Firestore requires a defense-in-depth approach with input validation, secure query construction, and proper security rules. Here are the specific techniques:
Input Validation and Whitelisting is the first line of defense. Never trust user input for field names or operators:
const VALID_FIELDS = ['name', 'email', 'role', 'status'];
const VALID_OPERATORS = ['==', '>', '<', '>=', '<='];
function validateQuery(field, operator, value) {
if (!VALID_FIELDS.includes(field)) {
throw new Error('Invalid field');
}
if (!VALID_OPERATORS.includes(operator)) {
throw new Error('Invalid operator');
}
// Additional validation based on field type
if (field === 'email' && !isValidEmail(value)) {
throw new Error('Invalid email format');
}
return { field, operator, value };
}
// Secure query construction
const { field, operator, value } = validateQuery(
req.query.field,
req.query.operator,
req.query.value
);
const query = db.collection('users').where(field, operator, value);
const results = await query.get();Parameterized Queries prevent injection by separating query structure from data:
// Always use parameterized queries
async function getUserByEmail(email) {
if (!isValidEmail(email)) {
throw new Error('Invalid email format');
}
const snapshot = await db.collection('users')
.where('email', '==', email)
.limit(1)
.get();
return snapshot.docs[0];
}
// Never construct queries with string concatenation
// Vulnerable - DO NOT USE
const unsafeQuery = `
SELECT * FROM users
WHERE ${field} ${operator} "${value}"
`;Secure Security Rules add a second layer of defense:
match /users/{userId} {
allow read: if
request.auth != null &&
isValidUserId(userId) &&
(request.auth.uid == userId ||
getRole(request.auth.uid) == 'admin');
allow write: if
request.auth != null &&
isValidUserId(userId) &&
request.auth.uid == userId;
}
function isValidUserId(userId) {
return userId.matches('^user_[a-zA-Z0-9_-]{10,20}$');
}
function getRole(userId) {
return get(/databases/$(database)/documents/users/$(userId)).data.role;
}Array Operation Security requires validating field names and values:
const VALID_ARRAY_FIELDS = ['roles', 'permissions', 'tags'];
async function safeArrayUpdate(docId, field, value) {
if (!VALID_ARRAY_FIELDS.includes(field)) {
throw new Error('Invalid array field');
}
// Validate value type
if (field === 'roles' && !isValidRole(value)) {
throw new Error('Invalid role');
}
const docRef = db.collection('users').doc(docId);
await docRef.update({
[field]: admin.firestore.FieldValue.arrayUnion(value)
});
}
async function safeArrayRemove(docId, field, value) {
if (!VALID_ARRAY_FIELDS.includes(field)) {
throw new Error('Invalid array field');
}
const docRef = db.collection('users').doc(docId);
await docRef.update({
[field]: admin.firestore.FieldValue.arrayRemove(value)
});
}Batch Operation Security requires validating all inputs before execution:
async function secureBatchUpdate(updates) {
const batch = db.batch();
const validUpdates = [];
for (const update of updates) {
// Validate document ID format
if (!isValidDocumentId(update.id)) {
throw new Error(`Invalid document ID: ${update.id}`);
}
// Validate field names and values
const docRef = db.collection('users').doc(update.id);
for (const [field, value] of Object.entries(update.changes)) {
if (!VALID_FIELDS.includes(field)) {
throw new Error(`Invalid field: ${field}`);
}
// Type validation
if (field === 'age' && typeof value !== 'number') {
throw new Error('Age must be a number');
}
}
batch.update(docRef, update.changes);
validUpdates.push(update.id);
}
await batch.commit();
return validUpdates;
}
function isValidDocumentId(id) {
// Firestore document IDs can't contain
// forward slashes, backslashes, or certain other characters
return /^[a-zA-Z0-9_-]{1,100}$/.test(id);
}Monitoring and Alerting should be implemented to detect injection attempts:
// Enhanced monitoring
const suspiciousPatterns = [
/\$\w+/, // Firestore operators
/\.\./, // Path traversal
/\b(or|and|not)\b/i // SQL-like keywords
];
function monitorQuery(query, userId) {
const queryStr = query._query.toString();
for (const pattern of suspiciousPatterns) {
if (pattern.test(queryStr)) {
console.warn(`Suspicious query detected from ${userId}:`, queryStr);
// Alert or block the query
return false;
}
}
return true;
}Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |