Broken Access Control in Firestore
How Broken Access Control Manifests in Firestore
Broken Access Control in Firestore occurs when applications fail to properly enforce who can read or write specific documents or collections. Unlike traditional databases with fixed schemas and table-level permissions, Firestore's flexible document model creates unique attack vectors that developers often overlook.
The most common Firestore-specific attack pattern is Collection-Level BOLA (Broken Object Level Authorization). Consider this vulnerable code:
app.get('/api/users/:userId/profile', async (req, res) => {
const { userId } = req.params;
const userDoc = await db.collection('users').doc(userId).get();
res.json(userDoc.data());
});This endpoint trusts the userId parameter without verifying the authenticated user owns that document. An attacker simply changes the ID in the URL to access any user's profile.
Document ID Enumeration is particularly dangerous in Firestore because document IDs are predictable. If your application uses auto-generated IDs or sequential IDs (like user emails or usernames), attackers can systematically request documents:
app.get('/api/posts/:postId', async (req, res) => {
const { postId } = req.params;
const postDoc = await db.collection('posts').doc(postId).get();
res.json(postDoc.data());
});Without checking post.authorId === req.user.uid, any authenticated user can read any post by guessing document IDs.
Subcollection Access creates another vulnerability. Developers often secure parent documents but forget that subcollections can be accessed independently:
// Vulnerable: Securing only the parent document
const userDoc = db.collection('users').doc(userId);
const messagesRef = userDoc.collection('messages');
// Attacker can still access: /users/ANOTHER_USER/messages/ANY_MESSAGE_IDFirestore Security Rules bypass is a critical issue. Many developers implement client-side checks but forget that Firestore Security Rules must be the final authority. Consider:
// Client-side check only - easily bypassed
const userDoc = await db.collection('users').doc(userId).get();
if (userDoc.data().ownerId !== req.user.uid) {
throw new Error('Unauthorized');
}
// Attacker modifies client or calls directly to FirestoreWithout proper Security Rules, this check provides zero protection.
Firestore-Specific Detection
Detecting Broken Access Control in Firestore requires both manual code review and automated scanning. middleBrick's Firestore-specific detection examines your API endpoints for common BOLA patterns and tests them systematically.
middleBrick's Firestore Detection Process:
The scanner identifies Firestore endpoints by analyzing request patterns and testing for predictable document ID manipulation. It attempts to access documents using IDs from other users, testing whether authorization checks exist. For example, if it detects an endpoint like /api/users/{id}/profile, it will:
- Extract the authenticated user's ID
- Replace it with another user's ID from the database
- Check if the response contains data from the wrong user
- Report the vulnerability with severity and remediation guidance
Manual Detection Checklist:
| Pattern | What to Test | Expected Behavior |
|---|---|---|
| Document ID in URL | Change ID to another user's ID | 403 Forbidden or 404 Not Found |
| Subcollection access | Access subcollections of other users | Access denied |
| Batch operations | Include documents from multiple users | Partial success or failure |
| Query parameters | Modify filters to include others' data | Only authorized data returned |
Firestore Security Rules Analysis: middleBrick can analyze your Security Rules configuration to identify gaps. Common issues include:
// Vulnerable: Allows anyone to read any document
match /users/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
// Better: Restrict both read and write
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}The scanner tests these rules by attempting unauthorized access patterns and verifying the rules block them.
Firestore-Specific Remediation
Fixing Broken Access Control in Firestore requires a defense-in-depth approach using both application-level checks and Firestore Security Rules.
Application-Level Fixes:
// Secure version with proper authorization
app.get('/api/users/:userId/profile', async (req, res) => {
const { userId } = req.params;
const { uid } = req.user;
// Verify user owns the document
if (userId !== uid) {
return res.status(403).json({ error: 'Forbidden' });
}
const userDoc = await db.collection('users').doc(userId).get();
if (!userDoc.exists) {
return res.status(404).json({ error: 'Not found' });
}
res.json(userDoc.data());
});Firestore Security Rules: This is your last line of defense:
// Secure Security Rules for users collection
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /users/{userId}/posts/{postId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
}
match /users/{userId}/messages/{messageId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
}
}
}Query-Level Authorization: When filtering data, ensure queries can't expose unauthorized documents:
// Vulnerable: Query returns all documents, client filters
const postsRef = db.collection('posts');
const postsQuery = postsRef.where('authorId', '==', req.user.uid);
// Secure: Use composite keys or security rules
const userPostsRef = db.collection('users')
.doc(req.user.uid)
.collection('posts');
// Or with security rules using getAfter()
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if resource.data.authorId == request.auth.uid;
allow write: if request.auth.uid == request.resource.data.authorId;
}
}
}Batch Operations: When performing batch writes, validate each document belongs to the user:
app.post('/api/users/:userId/update-profiles', async (req, res) => {
const { userId } = req.params;
const updates = req.body;
if (userId !== req.user.uid) {
return res.status(403).json({ error: 'Forbidden' });
}
const batch = db.batch();
const batchErrors = [];
for (const update of updates) {
const docRef = db.collection('users').doc(update.id);
const doc = await docRef.get();
if (doc.exists && doc.data().ownerId === req.user.uid) {
batch.update(docRef, update.data);
} else {
batchErrors.push(update.id);
}
}
await batch.commit();
res.json({ success: true, errors: batchErrors });
});Testing Your Fixes: After implementing fixes, use middleBrick to verify your APIs are now secure. The scanner will attempt the same attack patterns and should now receive 403 Forbidden responses instead of data exposure.
Frequently Asked Questions
Can Firestore Security Rules alone prevent all Broken Access Control?
Firestore Security Rules are essential but not sufficient alone. They provide the final security boundary, but application-level checks improve user experience by providing immediate feedback without round trips to Firestore. The best approach combines both: client-side validation for UX, application-level checks for business logic, and Security Rules as the ultimate authority.
How does middleBrick detect Broken Access Control in Firestore APIs?
middleBrick detects BOLA by systematically testing your endpoints with manipulated document IDs. It extracts authenticated user IDs from your API responses, then attempts to access documents using other users' IDs. If it receives data it shouldn't have access to, it flags this as a vulnerability. The scanner also analyzes your Security Rules configuration to identify gaps in your authorization model.