Insecure Design in Adonisjs with Firestore
Insecure Design in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Insecure design in an AdonisJS application that uses Google Cloud Firestore often stems from modeling data and access patterns that do not enforce authorization at the data layer. Firestore’s flexible document model can encourage storing user-scoped data in flat collections without enforcing ownership checks in every query. When route handlers or controller actions build queries using only client-supplied identifiers (such as a resource ID) without verifying that the authenticated user has permission to access that document, this becomes a Broken Object Level Authorization (BOLA) / Insecure Direct Object Reference (IDOR) pattern.
For example, an AdonisJS route like /projects/:projectId/members might directly use projectId to fetch a document from a projects collection. If the handler does not ensure the authenticated user is a member of that project, an attacker can iterate or guess project IDs and read or modify data they should not see. This risk is amplified when Firestore security rules rely on request.auth.uid but the application layer does not perform matching checks, leading to inconsistent enforcement.
Another insecure design pattern is trusting Firestore document IDs as authorization tokens. Document IDs are often predictable (e.g., push IDs or sequential values), and if the application assumes that possessing a document ID implies access, an attacker can leverage IDOR to traverse relationships. For instance, user profiles stored under a users collection with IDs derived from user registration order can be enumerated. Even when Firestore rules restrict reads to request.auth.uid, an AdonisJS service that passes attacker-controlled IDs directly to the backend can bypass intended boundaries if the service does not validate ownership.
Data exposure can also arise from how AdonisJS structures its models and services. If a service method returns entire Firestore documents without filtering sensitive fields (such as password hashes, internal flags, or PII), the API response may leak data. This is a data exposure design flaw: the application layer should project only necessary fields and apply strict allowlists before serialization. Similarly, insufficient rate limiting or missing validation on input identifiers can enable enumeration attacks, where attackers infer valid resource IDs by observing differences in response times or status codes.
These issues map to common frameworks such as the OWASP API Top 10 (2023), specifically Broken Object Level Authorization and Excessive Data Exposure. PCI-DSS, SOC 2, and GDPR also require that access controls be enforced consistently and that personal data be protected against unauthorized access. In AdonisJS with Firestore, this means designing services that treat Firestore document references as opaque tokens, always pairing application-level ownership checks with Firestore rules, and never relying on a single layer for authorization.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
To remediate insecure design when using AdonisJS with Firestore, enforce strict ownership checks in service methods and ensure query constraints always include the authenticated user’s UID. Below are concrete patterns that demonstrate secure handling of Firestore documents in AdonisJS controllers and services.
1. Parameterized query with ownership check
Always scope queries to the authenticated user. Do not trust route parameters alone. In an AdonisJS controller, bind the user ID from the authentication guard and use it as a Firestore query filter:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Project from 'App/Models/Project'
export default class ProjectsController {
public async show({ params, auth }: HttpContextContract) {
const user = auth.user!
const project = await Project.query()
.where('id', params.id)
.where('userId', user.id) // enforce ownership at query time
.preload('members')
.firstOrFail()
return project
}
}
This ensures that even if an attacker manipulates params.id, the query will return no results unless the document belongs to the authenticated user.
2. Firestore rule + service-layer validation alignment
Design Firestore security rules to match application guarantees. For a projects collection where only members can read documents, rules should reference a members subcollection and the AdonisJS service should verify membership before returning data:
// Firestore security rule (conceptual)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId} {
allow read: if request.auth != null
&& exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid));
allow write: if request.auth != null
&& exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid));
}
}
}
In AdonisJS, a service method can double-check membership by reading the membership subcollection:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class ProjectService {
public async userCanAccessProject(userId: string, projectId: string): Promise {
const memberDoc = await firestore
.doc(`projects/${projectId}/members/${userId})
.get()
return memberDoc.exists
}
}
This two-layer validation reduces the risk of misconfigured rules or client-side tampering.
3. Output filtering and field-level permissions
Prevent data exposure by selecting only required fields and removing sensitive data before serialization. Avoid returning entire Firestore documents directly from controllers:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class ProfilesController {
public async show({ params, auth }: HttpContextContract) {
const doc = await firestore.doc(`users/${params.id}`).get()
const data = doc.data()
if (!data || data.publicId !== auth.user?.publicId) {
throw new Error('Unauthorized')
}
// Return only safe fields
return {
publicId: data.publicId,
displayName: data.displayName,
email: data.email,
// Never include passwordHash, internalRole, or rawTokens
}
}
}
This approach enforces a strict allowlist and prevents accidental leakage of credentials or internal metadata.
4. Input validation and canonical resource references
Validate identifiers to avoid enumeration and ensure they are not guessable. Use UUIDs or opaque tokens instead of sequential IDs where appropriate, and normalize references in Firestore paths:
import { v4 as uuidv4 } from 'uuid'
export async function createProject(authUserId: string, payload: any) {
const projectId = uuidv4() // opaque, non-enumerable ID
await firestore.doc(`projects/${projectId}`).set({
id: projectId,
userId: authUserId,
name: payload.name,
createdAt: new Date(),
})
return projectId
}
By using non-sequential IDs and ensuring the backend owns the mapping between business keys and Firestore paths, you reduce the risk of IDOR via predictable references.