Cache Poisoning in Adonisjs with Firestore
Cache Poisoning in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Cache poisoning in an AdonisJS application that uses Google Cloud Firestore occurs when an attacker causes cached representations of Firestore query results or document-derived data to store attacker-controlled content. Because AdonisJS does not enforce strict cache key scoping by default, and Firestore data can include dynamic fields such as document IDs, timestamps, and user-specific attributes, improperly normalized cache keys can lead to one user’s view being served to another or to unauthenticated endpoints.
The risk is realized when cached responses are keyed by insufficient inputs, such as raw route parameters or non-unique user identifiers, and those keys are reused across contexts. For example, if an endpoint caches a Firestore document read by document ID without including the user or tenant identifier in the cache key, an authenticated user may receive another user’s cached document. In a black-box scan, middleBrick tests this by probing endpoints that interact with Firestore without authentication and checking whether cached or reflected data from one subject appears in another’s response.
Firestore-specific properties exacerbate this: document paths can contain dynamic segments (projectId, userId, documentId), and queries may return different subsets based on security rules that are not enforced at cache layer. If the cache stores rendered JSON that includes Firestore metadata such as readTime or updateTime, an attacker might manipulate query parameters to alter which document is retrieved and have that poisoned representation cached under a shared key. This can lead to IDOR-like exposure where one user’s data is cached and subsequently served to others, violating property authorization checks that middleBrick evaluates as part of its BOLA/IDOR and Property Authorization checks.
Because middleBrick scans the unauthenticated attack surface, it can detect endpoints that accept identifiers used to fetch Firestore documents without proper isolation in cache keys, and it flags missing input validation that could allow an attacker to select arbitrary document IDs. The scanner also checks for excessive agency patterns in any LLM-integrated workflows that might automate cache interactions, ensuring that unsafe consumption of Firestore-derived data does not propagate into model-driven logic.
Remediation guidance centers on deterministic, context-rich cache keys and strict validation of Firestore identifiers. Do not rely on client-supplied values alone; bind them to the requesting identity or tenant and normalize paths before caching. Use server-side, parameterized queries with bound values and avoid caching raw Firestore documents directly unless you include user and scope dimensions in the cache key.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
To remediate cache poisoning when using Firestore in AdonisJS, enforce strict cache key construction and validate all document identifiers on the server. Below are concrete, working examples that demonstrate secure patterns.
1. Build cache keys with user and scope context
Include the authenticated user ID (or tenant ID) and a normalized resource type when forming cache keys. This prevents cross-user contamination.
import { Cache } from '@ioc:Adonis/Core/Cache'
import { DateTime } from 'luxon'
export async function getDocumentWithCache(userId: string, docId: string) {
// Normalize input: ensure docId is a valid Firestore document ID pattern
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(docId)) {
throw new Error('Invalid document ID')
}
const cacheKey = `firestore:doc:${userId}:${docId}`
return Cache.getOrSet(cacheKey, async () => {
const doc = await firestore.doc(`users/${userId}/data/${docId}`).get()
if (!doc.exists) return null
// Return a clean, transformed object without internal metadata
return {
id: doc.id,
...doc.data(),
// Do not cache Firestore metadata such as readTime/updateTime
}
}, DateTime.local().plus({ minutes: 5 }).toJSDate())
}
2. Use parameterized Firestore queries and bound values
Avoid string concatenation when forming document paths or collection queries. Use Firestore’s built-in methods with bound parameters and validate inputs before passing them to Firestore.
import { collection, doc, getDoc, query, where, getDocs } from 'firebase/firestore'
export async function getUserDocumentBySafeQuery(tenantId: string, email: string) {
// Validate tenantId and email format to prevent injection via path segments
if (!/^[a-f0-9-]{36}$/.test(tenantId)) {
throw new Error('Invalid tenant identifier')
}
if (!/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)) {
throw new Error('Invalid email')
}
const q = query(collection(firestore, `tenants/${tenantId}/users`), where('email', '==', email))
const snapshot = await getDocs(q)
const results = snapshot.docs.map(d => ({ id: d.id, ...d.data() }))
return results
}
3. Isolate cache by request context and disable caching for sensitive operations
For endpoints that return user-specific Firestore data, ensure the cache key incorporates request context such as tenant or role. Avoid caching responses that contain sensitive Firestore metadata or that are derived from unvalidated parameters.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export async function profileHandler({ auth, cache }: HttpContextContract) {
const user = auth.user
if (!user) {
return { error: 'Unauthenticated' }
}
const key = `profile:${user.id}:v1`
return cache.getOrSet(key, async () => {
const doc = await firestore.doc(`users/${user.id}`).get()
return {
uid: doc.id,
email: doc.data()?.email,
// Exclude Firestore internal fields
}
}, DateTime.local().plus({ hours: 1 }).toJSDate())
}
4. Validate and sanitize inputs before using them in Firestore paths
Never directly interpolate request parameters into Firestore paths. Use allowlists and strict validation to ensure document IDs and collection names cannot be manipulated to access unrelated data.
export async function getSharedResource(tenantId: string, resourceType: string, resourceId: string) {
const allowedTypes = ['reports', 'settings', 'templates']
if (!allowedTypes.includes(resourceType)) {
throw new Error('Unsupported resource type')
}
// Ensure IDs are safe before constructing paths
const safeTenantId = tenantId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 36)
const safeResourceId = resourceId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 100)
const docRef = firestore.doc(`tenants/${safeTenantId}/${resourceType}/${safeResourceId}`)
const snap = await docRef.get()
return snap.exists ? snap.data() : null
}
By combining these patterns, AdonisJS applications reduce the risk of cache poisoning with Firestore-backed data. middleBrick will validate that cache keys incorporate user or tenant context, that inputs are normalized and validated, and that sensitive Firestore metadata is excluded from cached representations.