Nosql Injection in Feathersjs with Firestore
Nosql Injection in Feathersjs with Firestore — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time APIs that can be configured with multiple transport layers, including REST and Socket.io. When FeathersJS services are backed by Firestore, queries are often constructed directly from user-supplied parameters such as query, where, or select. If these parameters are merged into service find or aggregation pipelines without validation or parameterization, the application can become vulnerable to NoSQL Injection.
NoSQL Injection in this context occurs when an attacker is able to manipulate query operators or structure to read data beyond their permissions, bypass intended filters, or infer data existence. In FeathersJS with Firestore, this commonly maps to the where clause of a query. For example, an attacker might submit a payload like { "$ne": "" } for a user ID field, causing the query to return records that should be restricted. Firestore does not provide native query parameterization the way SQL prepared statements do; instead, query construction is programmatic, placing responsibility on the developer to sanitize and validate inputs.
Consider a FeathersJS service defined as follows:
const { Service } = require('feathersjs');
const admin = require('firebase-admin');
const db = admin.firestore();
class FirestoreBooksService extends Service {
async find(params) {
const { query = {} } = params;
const collection = db.collection('books');
let firestoreQuery = collection.where('tenantId', '==', params.account.tenantId);
if (query.title) {
firestoreQuery = firestoreQuery.where('title', '>=', query.title).where('title', '<=', query.title + '\uf8ff');
}
if (query.status) {
firestoreQuery = firestoreQuery.where('status', '==', query.status);
}
const snapshot = await firestoreQuery.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
}
If query.title is attacker-controlled and not validated, they could supply { "title": { "$gt": "" } } or similar operators to perform a range scan across all tenant data. Because Firestore evaluates these operators directly, the injected condition becomes part of the constructed query, potentially bypassing tenant isolation if tenantId checks are not strictly enforced for every condition. This maps to OWASP API Top 10 A1:2023 — Broken Object Level Authorization, and can be detected by middleBrick as a BOLA/IDOR or Property Authorization finding.
Other vectors include injection into aggregation pipelines or document updates. For instance, if a FeathersJS hook merges user input into an aggregation $match stage without validation, operators such as $or or $where can be abused to change logical intent. Because Firestore supports a broad set of query operators, unsanitized input can lead to data exposure or privilege escalation. middleBrick’s checks for Input Validation and BFLA/Privilege Escalation are designed to surface these risks in unauthenticated scans.
Additionally, Firestore security rules are not a substitute for server-side validation. Rules can reject writes, but they do not prevent overly broad read queries when the application itself constructs the query using attacker-influenced parameters. Therefore, NoSQL Injection in FeathersJS with Firestore is a product of dynamic query building on untrusted input, compounded by operator misuse and insufficient tenant scoping.
Firestore-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on strict input validation, canonical query construction, and tenant scoping for every Firestore operation. Avoid directly merging user input into Firestore query objects. Instead, use allowlists, type checks, and explicit operator mapping.
Below is a hardened version of the earlier FeathersJS service with Firestore. It validates title and status against allowlists, ensures tenant isolation is applied unconditionally, and avoids exposing operators from client input.
const allowedStatuses = ['available', 'reserved', 'archived'];
const ALLOW_TITLE_CHARS = /^[a-zA-Z0-9 _\-]{1,100}$/;
class FirestoreBooksService extends Service {
async find(params) {
const { query = {} } = params;
const collection = db.collection('books');
let firestoreQuery = collection.where('tenantId', '==', params.account.tenantId);
if (typeof query.title === 'string' && query.title.trim()) {
if (!ALLOW_TITLE_CHARS.test(query.title.trim())) {
throw new Error('Invalid title format');
}
const normalized = query.title.trim();
firestoreQuery = firestoreQuery
.where('title', '>=', normalized)
.where('title', '<=', normalized + '\uf8ff');
}
if (typeof query.status === 'string' && allowedStatuses.includes(query.status)) {
firestoreQuery = firestoreQuery.where('status', '==', query.status);
}
const snapshot = await firestoreQuery.get();
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
async create(data, params) {
if (!Array.isArray(data.tags) || data.tags.some(t => typeof t !== 'string')) {
throw new Error('Invalid tags');
}
return super.create({
...data,
tenantId: params.account.tenantId
}, params);
}
}
Key practices include:
- Never pass raw user objects into
where. Extract and validate each field individually. - Use allowlists for enumerated fields such as status or tags rather than denylists.
- Always enforce tenant isolation at the data access layer, not only in Firestore rules.
- For text search, prefer normalized prefix/suffix ranges instead of forwarding operators like
$regexor$where. - If complex querying is required, define server-side query presets keyed by identifier, rather than constructing queries from raw input.
These steps align with findings that map to OWASP API Top 10, PCI-DSS, and SOC2 controls, and they reduce the likelihood of NoSQL Injection as well as BOLA/IDOR. middleBrick’s scans can verify that such mitigations are in place by checking for input validation and property authorization patterns.