Insecure Direct Object Reference in Firestore
How Insecure Direct Object Reference Manifests in Firestore
Insecure Direct Object Reference (IDOR) in Firestore occurs when client applications can access or manipulate data by directly specifying document IDs or collection paths that they shouldn't have permission to access. Unlike traditional databases where you might need to guess table names, Firestore's document-based structure and client SDKs make IDOR attacks particularly straightforward.
The most common Firestore IDOR pattern involves directly exposing document IDs in URLs or client-side code. For example:
const docId = window.location.pathname.split('/')[2];
const docRef = firestore.collection('users').doc(docId);
const docSnapshot = await docRef.get();
An attacker can simply modify the docId parameter to access any user's document. If the document exists and the client has read permissions for the users collection, they'll receive the data without any authentication check against the document's owner.
Collection traversal attacks are another Firestore-specific IDOR variant. Developers often implement pagination or filtering by allowing clients to specify collection paths:
const collectionPath = req.query.collection || 'public';
const collectionRef = firestore.collection(collectionPath);
Attackers can manipulate the collection parameter to access sensitive collections like admin, payments, or userCredentials if those collections exist and the client has read permissions.
Firestore's document subcollections create additional IDOR opportunities. Consider this pattern:
const userId = req.params.userId;
const messageRef = firestore.collection('users').doc(userId).collection('messages');
If the application doesn't verify that the authenticated user owns the userId being accessed, attackers can read any user's messages by changing the ID in the URL.
Batch operations in Firestore can also introduce IDOR vulnerabilities. The getDocuments batch API allows reading multiple documents in one request:
const batchRef = firestore.collection('users').doc('userA').collection('data');
const snapshot = await batchRef.listDocuments();
If the application doesn't properly scope batch operations to the authenticated user's data, attackers can enumerate and access multiple documents simultaneously.
Firestore's security rules, while powerful, can be misconfigured in ways that enable IDOR. A common mistake is using document IDs in security rules without proper validation:
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
This rule appears secure but fails if the client can somehow influence the userId parameter or if the application constructs document references using untrusted input.
Firestore-Specific Detection
Detecting IDOR in Firestore requires both static analysis of your codebase and dynamic testing of your API endpoints. The most effective approach combines automated scanning with manual verification.
Static analysis should focus on identifying patterns where document IDs or collection paths are constructed from user input. Look for these code patterns:
// High-risk patterns
const ref = firestore.collection(collectionName).doc(docId);
const ref = firestore.doc(path);
const ref = firestore.collection(path);
Search your codebase for collection(), doc(), and doc() calls where the arguments come from HTTP parameters, URL paths, or client-side data. Pay special attention to dynamic path construction using string concatenation or template literals.
Dynamic testing with middleBrick can automatically detect Firestore IDOR vulnerabilities by testing unauthenticated access to your API endpoints. middleBrick's black-box scanning approach sends requests to your Firestore-backed API and analyzes the responses for data exposure patterns. The scanner tests for:
- Direct document ID manipulation in REST endpoints
- Collection path traversal attacks
- Subcollection access without proper authorization
- Batch operation vulnerabilities
- Security rule bypass attempts
middleBrick's Firestore-specific detection includes checking for common REST API patterns like:
GET /api/users/{userId}/profile
GET /api/documents/{docId}
GET /api/collections/{collectionName}/items
The scanner automatically attempts to access documents with common ID patterns, including numeric IDs, UUIDs, and sequential identifiers that might be guessable.
For comprehensive testing, use middleBrick's CLI tool to scan your development and staging environments before deployment:
npx middlebrick scan https://your-api.example.com
The tool provides a security score with specific findings for IDOR vulnerabilities, including the exact endpoints and parameters that are vulnerable.
Manual testing should complement automated scanning. Use tools like Postman or curl to systematically test your API endpoints with modified IDs and paths. Pay attention to HTTP status codes—returning 200 for non-existent documents but 404 for unauthorized access can leak information about valid document IDs.
Firestore-Specific Remediation
Remediating Firestore IDOR requires a defense-in-depth approach combining proper security rules, input validation, and secure application design patterns.
The foundation of Firestore security is robust security rules. Instead of relying on document ID matching, use field-based authorization:
match /users/{userId} {
allow read, write: if
request.auth.uid == userId ||
request.auth.token.admin == true;
}
match /users/{userId}/messages/{messageId} {
allow read, write: if
request.auth.uid == userId ||
request.auth.token.admin == true;
}
This approach ensures that users can only access their own documents, regardless of how the document ID is constructed in the client application.
For applications that need more flexible access patterns, implement server-side validation before constructing Firestore references:
async function getUserData(userId, authenticatedUserId) {
if (userId !== authenticatedUserId && !userIsAdmin(authenticatedUserId)) {
throw new Error('Unauthorized');
}
const userRef = firestore.collection('users').doc(userId);
const snapshot = await userRef.get();
return snapshot.exists ? snapshot.data() : null;
}
Always validate that the authenticated user has permission to access the requested resource before creating any Firestore references.
Implement input sanitization and validation for any user-provided paths or IDs:
function validateDocumentPath(path) {
const segments = path.split('/');
if (segments.length % 2 !== 1) {
throw new Error('Invalid path');
}
for (const segment of segments) {
if (segment.includes('../') || segment.includes('./')) {
throw new Error('Path traversal detected');
}
}
return true;
}
Use Firestore's built-in security features like request.auth.token to store user roles and permissions. This allows you to implement role-based access control directly in your security rules:
match /admin/{doc=**} {
allow read, write: if request.auth.token.admin == true;
}
For applications with complex authorization requirements, consider using a service account to proxy all database operations. This centralizes security logic and prevents client applications from having direct database access:
const admin = require('firebase-admin');
const db = admin.firestore();
async function secureReadDocument(collection, docId, userId) {
const docRef = db.collection(collection).doc(docId);
const snapshot = await docRef.get();
if (!snapshot.exists) {
return null;
}
const data = snapshot.data();
if (data.ownerId !== userId && !userIsAdmin(userId)) {
throw new Error('Unauthorized');
}
return data;
}
Implement comprehensive logging and monitoring for data access patterns. Set up Cloud Functions to log all database reads and writes, then use Cloud Logging to alert on suspicious access patterns like rapid enumeration of document IDs or access to collections outside normal usage patterns.
Regularly audit your Firestore security rules using the Firebase emulator suite. Test your rules against various access patterns to ensure they properly restrict data access:
const rules =
'match /users/{userId} {\n'
' allow read, write: if request.auth.uid == userId;\n'
'}';
const testCases = [
{
description: 'User can read own data',
request: { auth: { uid: 'userA' }, path: 'users/userA' },
response: { allowRead: true }
},
{
description: 'User cannot read others data',
request: { auth: { uid: 'userA' }, path: 'users/userB' },
response: { allowRead: false }
}
];
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |