Injection Flaws in Firestore
How Injection Flaws Manifests in Firestore
Injection flaws in Firestore occur when untrusted data is improperly handled in database queries, allowing attackers to manipulate query logic and access unauthorized data. Unlike SQL injection, Firestore uses a document-oriented model with query builders, but the injection vectors are equally dangerous.
The most common Firestore injection pattern involves dynamic query construction. Consider this vulnerable code:
const userId = req.query.userId; // untrusted input
const snapshot = await db.collection('users')
.where('id', '==', userId)
.get();
If userId contains special characters like || or path traversal sequences, an attacker could craft queries that bypass authorization. Firestore's query language interprets certain characters and operators that can be exploited.
Another critical vector is collection name injection. When collection names are constructed dynamically:
const collectionName = req.query.collection;
const docId = req.query.id;
const doc = await db.collection(collectionName).doc(docId).get();
An attacker could supply collection=__meta__/documents to access Firestore's internal metadata collections, potentially exposing schema information or configuration data.
Firestore also has injection-like issues with field path resolution. When using update() or set() with dynamic field paths:
const fieldPath = req.body.field;
const value = req.body.value;
await db.collection('users').doc(userId).update({ [fieldPath]: value });
This allows attackers to update arbitrary fields, including nested fields they shouldn't access, by crafting malicious field path strings.
Document ID injection is another vector. When document IDs are constructed from user input:
const userId = req.body.userId;
const documentId = `user-${userId}`;
const doc = await db.collection('users').doc(documentId).get();
An attacker could use userId= to access the root document or use path traversal characters to access unintended documents.
Firestore's security rules can also be bypassed through injection when queries are constructed dynamically. If rules rely on query parameters that can be manipulated, attackers can craft queries that return unauthorized data even when rules appear correct.
Firestore-Specific Detection
Detecting injection flaws in Firestore requires both static code analysis and dynamic runtime testing. The key is identifying where untrusted data flows into query construction.
Static analysis should flag these patterns:
// Vulnerable: dynamic collection names
const col = db.collection(userInput);
// Vulnerable: dynamic field paths
const field = userInput;
doc.update({ [field]: value });
// Vulnerable: dynamic document IDs
const docId = userInput;
db.collection('users').doc(docId);
Dynamic detection with middleBrick scans APIs that interact with Firestore and tests for injection vulnerabilities by injecting special characters and path traversal sequences. The scanner tests:
- Collection name manipulation with
__meta__,__system__, and path traversal sequences - Field path injection with special characters like
.,/, and array indices - Document ID manipulation including empty strings and special characters
- Query parameter injection testing boolean operators and comparison operators
middleBrick's Firestore-specific checks include testing for BOLA (Broken Object Level Authorization) by attempting to access documents with manipulated IDs and testing privilege escalation by attempting to update fields that should be immutable.
Runtime monitoring should also watch for:
// Monitor for suspicious query patterns
const suspiciousQueries = [
{ pattern: /__meta__/i, reason: 'Metadata collection access' },
{ pattern: ///, reason: 'Path traversal' },
{ pattern: / /, reason: 'Null byte injection' }
];
Security rules should be tested with automated tools that attempt to bypass them through query manipulation, ensuring that even if injection occurs, data remains protected.
Firestore-Specific Remediation
Remediating Firestore injection flaws requires a defense-in-depth approach combining input validation, query parameterization, and security rules.
First, implement strict input validation and whitelisting:
function validateCollectionName(name) {
const validCollections = ['users', 'posts', 'comments'];
if (!validCollections.includes(name)) {
throw new Error('Invalid collection');
}
return name;
}
function validateUserId(id) {
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
throw new Error('Invalid user ID format');
}
return id;
}
Second, use parameterized queries where possible. While Firestore doesn't have traditional prepared statements, you can structure queries to avoid dynamic construction:
// Safe: predefined queries
const getUserById = async (db, userId) => {
const snapshot = await db.collection('users')
.where('id', '==', userId)
.get();
return snapshot.docs.map(doc => doc.data());
};
// Safe: validate before use
const safeGetDocument = async (db, collection, docId) => {
const validatedCollection = validateCollectionName(collection);
const validatedId = validateUserId(docId);
const doc = await db.collection(validatedCollection).doc(validatedId).get();
return doc.exists ? doc.data() : null;
};
Third, implement comprehensive security rules that don't rely on query parameters:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /posts/{postId} {
allow read: if true;
allow write: if request.auth.uid == request.resource.data.authorId;
}
match /{document=**} {
allow read, write: if false;
}
}
}
Fourth, use Firestore's built-in validation features:
const validateDocument = (data, allowedFields) => {
const unknownFields = Object.keys(data).filter(
field => !allowedFields.includes(field)
);
if (unknownFields.length > 0) {
throw new Error(`Unknown fields: ${unknownFields.join(', ')}`);
}
};
// Usage
app.post('/api/users/:id', async (req, res) => {
const { id } = req.params;
const userData = req.body;
validateDocument(userData, ['name', 'email', 'age']);
validateUserId(id);
try {
await db.collection('users').doc(id).set(userData);
res.status(201).send();
} catch (error) {
res.status(400).send({ error: error.message });
}
});
Finally, implement logging and monitoring for suspicious query patterns:
const suspiciousQueryPatterns = [
/__meta__/,
/\//,
/\u0000/,
/\.\./
];
const logSuspiciousQuery = (query, userId) => {
const suspicious = suspiciousQueryPatterns.some(pattern =>
pattern.test(query.toString())
);
if (suspicious) {
console.warn(`Suspicious query detected`, {
userId,
query: query.toString(),
timestamp: new Date().toISOString()
});
}
};