Insecure Direct Object Reference in Adonisjs with Firestore
Insecure Direct Object Reference in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an internal object—such as a Firestore document ID—and allows an authenticated user to access or modify that object without verifying that the user has permission for that specific resource. AdonisJS, a Node.js web framework, does not inherently enforce resource ownership; it relies on developer code to perform authorization checks. When integrating Firestore, developers often use user-provided identifiers (e.g., document IDs from route parameters) directly in Firestore get/query operations. If these identifiers are used without validating that the authenticated subject owns or is authorized to access the target document, an IDOR vulnerability exists.
Consider a route like GET /api/users/:userId/profile in AdonisJS. If the handler retrieves the profile by concatenating the userId from params with a Firestore path such as users/${userId}/profile and returns the document without confirming that the authenticated user matches userId, an attacker can iterate over known user IDs and read other users’ profiles. Firestore security rules can mitigate some risks, but they are not a substitute for application-level authorization; rules may be misconfigured or bypassed if the request is authenticated and the document exists. Additionally, Firestore rules cannot enforce complex ownership checks across collections without carefully crafted request-time validation, which is often missing in naive AdonisJS integrations.
Another common pattern involves list endpoints that expose references to Firestore documents. For example, an endpoint returning a list of documents with IDs embedded in the response can enable attackers to modify those IDs and probe for unauthorized access. Because Firestore document IDs are often predictable (e.g., auto-generated or sequential), the attack surface expands. The combination of AdonisJS routing that trusts client-supplied identifiers and Firestore’s flexible document addressing creates a scenario where IDOR can affect not only direct document reads but also operations that update or delete referenced resources, such as changing a user’s role or escalating privileges if the handler performs writes based on unchecked IDs.
Moreover, Firestore’s real-time listeners and subcollection structures can compound IDOR risks. If an AdonisJS endpoint attaches a listener using a user-supplied document path without validating ownership, other authenticated users might receive updates meant for a different resource. Similarly, subcollections (e.g., users/{userId}/orders/{orderId}) require checks at both the parent and child levels. An application that validates only the parent document (e.g., confirming the user owns users/{userId}) but fails to validate access to orders/{orderId} can still leak or allow unauthorized modifications to order data. This highlights the need for explicit, per-request authorization that aligns the authenticated identity with the specific Firestore document paths being accessed or modified.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
Remediation centers on ensuring that every Firestore operation is gated by explicit authorization that ties the authenticated user to the target document. Below are concrete patterns and code examples for AdonisJS that address IDOR when working with Firestore.
1. Validate user identity against Firestore document ownership
For endpoints that accept a user-provided identifier, retrieve the Firestore document and compare its ownership field (e.g., uid) with the authenticated user’s ID before returning data.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Firestore, doc, getDoc } from 'firebase-admin/firestore'
export default class UserProfilesController {
constructor(private db: Firestore) {}
public async showProfile({ params, auth }: HttpContextContract) {
const userId = auth.user?.id
const targetUserId = params.userId
if (!userId || userId !== targetUserId) {
throw new Error('Unauthorized')
}
const profileRef = this.db.doc(`users/${targetUserId}/profile`)
const snapshot = await getDoc(profileRef)
if (!snapshot.exists()) {
throw new Error('Not found')
}
return snapshot.data()
}
}
2. Use Firestore security rules in tandem with application checks
Although rules are not a replacement for application-level checks, they provide a safety net. Define rules that scope documents to authenticated users and enforce ownership via request.auth.uid.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /profile/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
}
3. Parameterize queries and avoid direct concatenation
Instead of constructing paths from raw params, use the authenticated user’s ID to scope queries and validate that returned documents belong to the requester.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Firestore, collection, query, where, getDocs } from 'firebase-admin/firestore'
export default class OrdersController {
constructor(private db: Firestore) {}
public async listOrders({ auth }: HttpContextContract) {
const userId = auth.user?.id
if (!userId) {
throw new Error('Unauthorized')
}
const q = query(collection(this.db, `users/${userId}/orders`), where('status', '==', 'pending'))
const snapshot = await getDocs(q)
return snapshot.docs.map(d => ({ id: d.id, ...d.data() }))
}
}
4. Validate IDs for sensitive operations
For operations that modify or delete, repeat ownership checks and consider using Firestore transactions to ensure consistency.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Firestore, doc, getDoc, updateDoc } from 'firebase-admin/firestore'
export default class RolesController {
constructor(private db: Firestore) {}
public async updateRole({ params, auth, request }: HttpContextContract) {
const userId = auth.user?.id
const targetUserId = params.userId
if (!userId || userId !== targetUserId) {
throw new Error('Unauthorized')
}
const userRef = doc(this.db, 'users', targetUserId)
const snap = await getDoc(userRef)
if (!snap.exists()) {
throw new Error('User not found')
}
await updateDoc(userRef, { role: request.input('role') })
return { success: true }
}
}
5. Avoid exposing Firestore document IDs in APIs
When returning lists, consider using opaque identifiers or mapping Firestore document IDs to internal references that are not predictable. This reduces the surface for ID enumeration.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Firestore, collection, getDocs } from 'firebase-admin/firestore'
export default class PublicProfilesController {
constructor(private db: Firestore) {}
public async index() {
const snapshot = await getDocs(collection(this.db, 'publicProfiles'))
return snapshot.docs.map(d => ({ id: d.id, ...d.data() }))
}
}
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 |
Frequently Asked Questions
Can Firestore security rules alone prevent IDOR in AdonisJS?
How can I test for IDOR in my AdonisJS and Firestore integration?
middlebrick scan <url> to identify exposed references and missing authorization checks. Combine this with targeted manual tests by modifying user-supplied identifiers (e.g., userId in routes) to verify that access is properly restricted per authentication context.