Beast Attack in Nestjs with Firestore
Beast Attack in Nestjs with Firestore — how this specific combination creates or exposes the vulnerability
A Beast Attack (Bypass of Level Security) in a NestJS application using Cloud Firestore occurs when authorization checks are implemented at the application layer but not enforced at the database or query layer. Firestore security rules are the primary enforcement mechanism; if rules are misaligned with business logic, an attacker can manipulate NestJS routes or services to access documents they should not see or modify.
Consider a typical NestJS service that queries Firestore using the Firebase Admin SDK. If the service trusts request parameters (such as :id) and builds a document reference without validating ownership or scope, the route becomes a horizontal privilege escalation vector. For example, an endpoint like GET /projects/:projectId/tasks/:taskId may retrieve a task document, but if Firestore rules only check project-level read access and the NestJS service does not re-validate that the authenticated user belongs to the requested project, a user can change :projectId to access tasks in other projects. This is a Beast Attack because the server-side logic bypasses the intended level of security provided by Firestore rules.
Firestore’s rule syntax is intentionally declarative and does not enforce user context unless explicitly modeled. If rules rely only on request.auth != null without checking request.auth.uid against document fields (e.g., request.auth.uid == resource.data.ownerId), an authenticated attacker can exploit weak rule composition. In NestJS, this is exacerbated when services construct references dynamically (e.g., doc(db, `projects/${projectId}/tasks/${taskId}`)) without ensuring projectId maps to an allowed set for the user. Attackers can iterate over plausible IDs or leverage information leakage to enumerate resources, demonstrating why runtime findings from scans that test BOLA/IDOR and Property Authorization are critical.
Additionally, Firestore’s indexing and querying behavior can unintentionally expose data if rules allow broad list operations. A NestJS endpoint that calls collection(groupRef, 'tasks') where groupRef derives from user input may return more documents than intended if Firestore rules permit it. Without server-side validation of query constraints and strict rule conditions that mirror access checks, the API surface expands, increasing the likelihood of successful Beast Attacks. Regular scanning with a tool that tests unauthenticated and authenticated scenarios helps surface these misalignments between NestJS application logic and Firestore rules.
Firestore-Specific Remediation in Nestjs — concrete code fixes
Remediation centers on aligning Firestore security rules with NestJS authorization logic and ensuring every query is constrained by user identity and tenant context. Below are concrete patterns and code examples.
1. Use Firestore rules to enforce ownership and tenant scope
Define rules that require user identifiers to match document fields. For a tasks collection under projects, enforce that only project members can read/write tasks.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /projects/{projectId}/tasks/{taskId} {
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.ownerId
&& request.auth.token.projects != null
&& request.auth.token.projects.hasAny([projectId]);
}
}
}
This ensures that even if a NestJS endpoint receives a :projectId and :taskId, Firestore will reject access unless the user’s token includes the project and the document’s ownerId matches.
2. Validate and bind user context in NestJS services
In your NestJS service, always derive document references from authenticated claims rather than trusting route parameters alone. Use the Firebase Admin SDK to verify ID tokens and extract custom claims that encode tenant membership.
import { Injectable } from '@nestjs/common';
import { getFirestore, doc, getDoc } from 'firebase-admin/firestore';
@Injectable()
export class TasksService {
async getTask(projectId: string, taskId: string, userId: string) {
const db = getFirestore();
const taskRef = doc(db, `projects/${projectId}/tasks/${taskId}`);
const snapshot = await getDoc(taskRef);
if (!snapshot.exists()) {
throw new NotFoundException('Task not found');
}
// Enforce ownership/tenant check in application layer as defense-in-depth
if (snapshot.data().ownerId !== userId) {
throw new ForbiddenException('Access denied to task');
}
return snapshot.data();
}
}
Here, userId should be derived from the verified Firebase ID token (e.g., via a guard that attaches auth context to the request). This mirrors Firestore rule checks and prevents path traversal across projects.
3. Constrain queries with collection group rules and tenant fields
If you use collection groups, pair them with tenant fields and strict rules. For example, store tenantId in each task and in user claims, then restrict queries.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{taskId} {
allow list: if request.auth != null
&& request.auth.token.tenantId == resource.data.tenantId;
allow get: if request.auth != null
&& request.auth.uid == resource.data.ownerId;
}
}
}
In NestJS, avoid generic collection calls without tenantId filters. Instead, scope queries explicitly:
import { collection, query, where, getDocs } from 'firebase-admin/firestore';
async function listTasksForTenant(db, tenantId: string, userId: string) {
const q = query(collection(db, 'tasks'), where('tenantId', '==', tenantId));
const snapshot = await getDocs(q);
// Further filter by ownerId in application layer if needed
return snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
}
This two-layer approach (rules + query constraints) reduces the impact of misconfigured inputs and aligns with the principle of least privilege.
4. Use custom claims for role and project membership
Store minimal, trusted metadata in ID token custom claims (e.g., projects array, roles). Firestore rules can reference these without additional lookups, improving performance and consistency.
// Admin SDK: set custom claims
await admin.auth().setCustomUserClaims(uid, {
email: user.email,
projects: ['proj-123', 'proj-456'],
role: 'member'
});
Rules can then use request.auth.token.projects.hasAny([projectId]) to enforce multi-tenant boundaries efficiently.
5. Monitor and test with security scans
Use scans that test BOLA/IDOR, Property Authorization, and Unsafe Consumption to detect rule misconfigurations and runtime exposure. Remediation guidance from these findings should map to OWASP API Top 10 and compliance frameworks such as PCI-DSS and SOC2.