Integrity Failures in Feathersjs with Firestore
Integrity Failures in Feathersjs with Firestore — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for creating JavaScript APIs with services that map closely to resources. When using Firestore as the backing store, integrity failures arise when application-level authorization and data validation are incomplete or inconsistent, allowing one service to read or modify records it should not. Firestore’s flexible data model and security rules can inadvertently permit cross-tenant data access or unsafe updates when FeathersJS hooks and services do not enforce strict ownership and schema checks.
Consider a multi-tenant application where each tenant’s data is isolated by a tenantId field. If a FeathersJS service for messages does not scope queries by tenantId, an authenticated user could provide a different tenantId in query parameters or manipulate the request payload to access another tenant’s documents. Firestore rules alone are insufficient if the client SDK sends unrestricted queries; the server-side service must enforce tenant scoping. Without this, an attacker can leverage IDOR (Insecure Direct Object Reference) to enumerate or modify records across tenant boundaries.
Integrity failures also occur when Firestore documents contain references that FeathersJS services do not validate. For example, a posts service may accept a categoryId in create or patch requests. If the service does not verify that the referenced category exists and belongs to the same tenant, an attacker can assign arbitrary or malicious categories, leading to inconsistent state or privilege escalation. Firestore allows updates to nested fields and map merges; if FeathersJS uses update with user-provided data without filtering allowed fields, an attacker can overwrite sensitive attributes such as isAdmin or billingRole.
Real-world attack patterns mirror general OWASP API Top 10 A01:2023 broken object level authorization and A03:2023 injection. In Firestore, injection takes the form of maliciously crafted updates that modify fields beyond intended scope, especially when combined with Firestore’s dot notation for nested fields (e.g., metadata.admin). CVE-classic examples of insecure direct object reference and mass assignment apply here: an attacker who can manipulate identifiers or patch fields can achieve horizontal or vertical privilege escalation across tenant data.
Property authorization is another critical dimension. Firestore documents often store arrays or maps representing user roles or permissions. If FeathersJS does not validate that changes to these properties respect business rules (e.g., only admins can promote users), a regular user could submit a PATCH request that adds themselves to an admin array. Firestore’s server-side rules might block direct writes, but if FeathersJS applies a merge without property-level checks, the update can succeed on the server, bypassing intended safeguards.
To detect these issues, middleBrick scans unauthenticated and authenticated attack surfaces, testing query manipulation, IDOR across resource IDs, and patch payloads that attempt unauthorized field updates. The scanner correlates findings with OpenAPI specs when available, mapping insecure endpoints to relevant OWASP API Top 10 categories and highlighting gaps in tenant isolation and property-level authorization.
Firestore-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on enforcing tenant scoping, validating references, and strictly controlling which fields can be updated. Always derive tenant context from the authenticated user or from a verified subdomain/path parameter, never from client-supplied query fields alone.
1. Tenant-scoped queries in FeathersJS services
Ensure every find, get, create, and patch operation includes a tenantId filter derived from the authenticated user’s claims. Avoid passing query parameters that could be tampered with to override the tenant filter.
// services/messages/messages.class.js
const { Service } = require('feathersjs');
class MessagesService extends Service {
async find(params) {
const { user } = params;
if (!user || !user.tenantId) {
throw new Error('Unauthorized');
}
// scoped query to Firestore via custom paginator or hook
params.query = {
...params.query,
tenantId: user.tenantId
};
return super.find(params);
}
async get(id, params) {
const { user } = params;
const record = await super.get(id, params);
if (record.tenantId !== user.tenantId) {
throw new Error('Unauthorized');
}
return record;
}
}
module.exports = function (app) {
app.use('/messages', new MessagesService({
Model: app.get('firestore'), // Firestore instance
paginate: { default: 10, max: 50 }
}));
};
2. Validate referenced documents and enforce ownership
When accepting foreign keys such as categoryId, verify existence and tenant ownership before allowing the operation. Use Firestore transactions or batched reads to avoid race conditions.
// services/posts/posts.class.js with validation
const { Service } = require('feathersjs');
class PostsService extends Service {
async create(data, params) {
const { user } = params;
const db = app.get('firestore');
const categoryRef = db.collection('categories').doc(data.categoryId);
const snap = await categoryRef.get();
if (!snap.exists || snap.data().tenantId !== user.tenantId) {
throw new Error('Invalid or unauthorized category');
}
return super.create({
...data,
tenantId: user.tenantId
}, params);
}
async patch(id, data, params) {
const { user } = params;
if (data.categoryId) {
const db = app.get('firestore');
const categoryRef = db.collection('categories').doc(data.categoryId);
const snap = await categoryRef.get();
if (!snap.exists || snap.data().tenantId !== user.tenantId) {
throw new Error('Invalid or unauthorized category');
}
}
return super.patch(id, data, params);
}
}
3. Strict field whitelisting for updates to prevent mass assignment
Firestore’s dot notation can update nested fields unexpectedly. Define a writable fields list and reject any keys not explicitly allowed.
// FeathersJS hooks to sanitize updates
const { iff, isProvider } = require('feathers-hooks-common');
function sanitizeUpdateAllowedFields(allowedFields) {
return context => {
if (isProvider('external', context)) {
const data = context.data;
const sanitized = {};
allowedFields.forEach(field => {
if (Object.prototype.hasOwnProperty.call(data, field)) {
sanitized[field] = data[field];
}
});
// preserve nested updates under allowed paths only
context.data = sanitized;
}
return context;
};
}
app.use('/posts', new PostsService({ /* options */ }));
app.service('posts').hooks({
before: {
update: [iff(isProvider('external'), sanitizeUpdateAllowedFields(['title', 'content', 'categoryId']))],
patch: [iff(isProvider('external'), sanitizeUpdateAllowedFields(['title', 'content', 'categoryId']))]
}
});
4. Use Firestore rules as a safety net, not the primary guard
Rules should complement server-side checks, not replace them. Define rules that require request.auth != null and validate that request.auth.uid matches the document’s tenantId or owner UID. Ensure rules reject writes that attempt to modify immutable fields such as created timestamps or admin flags.
// Firestore security rules example
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{post} {
allow read, write: if request.auth != null && request.auth.uid == request.resource.data.tenantId;
allow create: if request.auth != null && request.resource.data.keys().hasAll(['title', 'content', 'categoryId', 'tenantId'])
&& request.resource.data.tenantId is string
&& request.resource.data.categoryId is string;
// prevent overwriting immutable fields
allow update: if request.auth != null && request.resource.data.diff(request.resource.data).affectedKeys().hasOnly(['title', 'content', 'categoryId']);
}
}
}
By combining tenant-aware services, strict validation of references, and field-level update controls, you mitigate integrity failures specific to the FeathersJS + Firestore stack. Regular scans with middleBrick help verify that these controls are effective across endpoints and that no new IDOR or mass assignment paths are introduced.