HIGH broken access controlfeathersjsfirestore

Broken Access Control in Feathersjs with Firestore

Broken Access Control in Feathersjs with Firestore — how this specific combination creates or exposes the vulnerability

Broken Access Control in a Feathersjs service backed by Firestore often stems from relying solely on client-side rules or incomplete server-side authorization. Feathersjs is a framework that encourages a service-oriented architecture where each service can define its own hooks and filters. If a service does not enforce identity and authorization checks within a before hook, and instead depends on Firestore Security Rules to block unauthorized reads or writes, an attacker can bypass these rules by calling the Feathers endpoint directly.

For example, consider a Feathers service for user profiles that exposes a find endpoint without validating that the requesting user can only retrieve their own document. Firestore rules might restrict reads to documents where userId == request.auth.uid, but if the Feathers service query does not include that filter, the rules alone do not prevent the request from reaching Firestore with an un scoped query. Because Firestore evaluates rules per document, a broad query from the server can return multiple documents if rules are misconfigured or if rules do not apply to server-side Admin SDK usage. The Admin SDK typically bypasses rules, so a Feathers service using the Admin SDK must enforce access controls explicitly in code.

Another common pattern is using Firestore for multi-tenant data without scoping queries by tenant identifier. A Feathers service might accept a tenantId from the client and use it to construct a query, but if the service does not verify that the authenticated user belongs to that tenant, horizontal privilege escalation occurs. Attackers can modify the tenantId in the request to access data belonging to other tenants, provided the Firestore rules do not explicitly prevent it and the service does not validate tenant membership.

Additionally, mass assignment risks in Firestore can amplify Broken Access Control. If a Feathers create or update method passes user-supplied JSON directly to the Firestore SDK without filtering sensitive fields like isAdmin or roles, users can elevate their privileges by including those fields in the payload. Proper server-side schema validation and field filtering are required to ensure that only intended fields are written.

These issues are detectable by security scans that compare runtime behavior against the API specification and Firestore rules. Findings typically highlight missing ownership checks, overly permissive rules, and unvalidated input in service methods. Prioritized remediation focuses on enforcing server-side authorization, scoping queries by user and tenant, and validating and sanitizing all inputs before they reach Firestore.

Firestore-Specific Remediation in Feathersjs — concrete code fixes

To remediate Broken Access Control in Feathersjs with Firestore, implement explicit authorization in service hooks, scope all queries by user and tenant, and validate inputs rigorously. Below are concrete, working code examples for Feathers services using the Firebase Admin SDK.

1. Scoped queries with ownership checks

Ensure that every query is scoped to the authenticated user. Do not rely on Firestore rules alone when using the Admin SDK.

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const admin = require('firebase-admin');

admin.initializeApp();
const db = admin.firestore();

const app = express(feathers());

app.use('/profiles', {
  async find(params) {
    const { user } = params;
    if (!user || !user.id) {
      throw new Error('Unauthenticated');
    }
    const snapshot = await db.collection('profiles')
      .where('userId', '==', user.id)
      .get();
    const profiles = [];
    snapshot.forEach(doc => profiles.push({ id: doc.id, ...doc.data() }));
    return profiles;
  },
  async get(id, params) {
    const { user } = params;
    if (!user || !user.id) {
      throw new Error('Unauthenticated');
    }
    const doc = await db.collection('profiles').doc(id).get();
    if (!doc.exists) {
      throw new Error('Not found');
    }
    const data = doc.data();
    if (data.userId !== user.id) {
      throw new Error('Forbidden');
    }
    return { id: doc.id, ...data };
  }
});

2. Tenant-aware queries with membership validation

For multi-tenant setups, validate tenant membership before querying tenant-scoped data.

app.use('/tenant-data', {
  async find(params) {
    const { user, query } = params;
    if (!user || !user.id) {
      throw new Error('Unauthenticated');
    }
    const tenantId = query.tenantId;
    if (!tenantId) {
      throw new Error('tenantId is required');
    }
    // Assume a membership collection documents tenant-user relationships
    const membershipDoc = await db.collection('memberships')
      .where('userId', '==', user.id)
      .where('tenantId', '==', tenantId)
      .limit(1)
      .get();
    if (membershipDoc.empty) {
      throw new Error('Forbidden: tenant membership not found');
    }
    const snapshot = await db.collection('tenantData')
      .where('tenantId', '==', tenantId)
      .get();
    const results = [];
    snapshot.forEach(doc => results.push({ id: doc.id, ...doc.data() }));
    return results;
  }
});

3. Strict input validation and mass assignment protection

Filter incoming data to prevent privilege escalation via mass assignment.

const safeUpdateFields = ['displayName', 'email'];

app.use('/users', {
  async patch(id, data, params) {
    const { user } = params;
    if (!user || !user.id) {
      throw new Error('Unauthenticated');
    }
    // Ensure users can only update their own profile
    if (id !== user.id) {
      throw new Error('Forbidden');
    }
    // Whitelist editable fields
    const updateData = {};
    safeUpdateFields.forEach(field => {
      if (data[field] !== undefined) {
        updateData[field] = data[field];
      }
    });
    await db.collection('users').doc(id).update(updateData);
    return { id, ...updateData };
  }
});

4. Hooks integration for centralized enforcement

Use Feathers hooks to apply authorization consistently across services.

const { iff, isProvider, populateExpando } = require('feathers-hooks-common');

function ensureOwnProfile() {
  return async context => {
    const { user, result } = context;
    if (isProvider('external') && user) {
      if (result && result.userId && result.userId !== user.id) {
        throw new Error('Forbidden');
      }
      if (Array.isArray(result)) {
        result.data = result.data.filter(item => item.userId === user.id);
      }
    }
    return context;
  };
}

app.service('profiles').hooks({
  before: {
    all: [iff(isProvider('external'), ensureOwnProfile())]
  }
});

By combining scoped Firestore queries, tenant validation, strict input filtering, and Feathers hooks, you reduce the attack surface for Broken Access Control. These patterns ensure that authorization is enforced in application logic rather than relying on rules alone, which is critical when using the Admin SDK that bypasses security rules.

Frequently Asked Questions

Why can't Firestore Security Rules alone protect server-side Admin SDK calls in Feathersjs?
The Admin SDK bypasses Firestore Security Rules, so rules do not restrict operations performed by server code. Feathers services must enforce their own authorization when using the Admin SDK to ensure users can only access data they are permitted to see.
How does scoping queries by userId and tenantId mitigate Broken Access Control?
Scoping queries ensures that even if a request is authenticated, it cannot retrieve records outside the user's scope or tenant. This prevents horizontal and vertical privilege escalation by limiting result sets and validating membership before data access.