HIGH insecure direct object referencefeathersjsfirestore

Insecure Direct Object Reference in Feathersjs with Firestore

Insecure Direct Object Reference in Feathersjs with Firestore — how this specific combination creates or exposes the vulnerability

Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an internal object—such as a Firestore document ID—and allows an authenticated subject to perform unauthorized operations on that object. When Feathersjs is used with Firestore, the risk arises from typical Feathers patterns that accept user-supplied IDs (e.g., via REST query params, route parameters, or payload IDs) and pass them directly to Firestore without ensuring the requesting subject has permission to access that specific document.

Consider a Feathers service defined for user profiles:

// services/profiles/profiles.service.js
const { Service } = require('feathersjs');
const admin = require('firebase-admin');
const db = admin.firestore();

class ProfilesService extends Service {
  async get(id, params) {
    const doc = await db.collection('profiles').doc(id).get();
    if (!doc.exists) {
      throw new Error('Not found');
    }
    return doc.data();
  }
}
module.exports = ProfilesService;

If the get method is invoked with an id supplied directly by the client (for example, /profiles/abc123), and the service does not verify that the authenticated user is allowed to view profile abc123, an IDOR exists. An attacker who knows or guesses another user’s document ID can retrieve or manipulate it by changing the ID in the request. Because Firestore enforces only document-level security rules (which may not be aligned with application ownership semantics), a service that does not enforce per-request authorization can inadvertently expose data across users.

In a Feathers application, IDOR can also manifest in find or patch operations when filters or IDs are not scoped to the requesting user. For example, a find without a default query filter could return multiple documents, and if the client can also supply a _id or id in the payload, they might escalate access:

// services/messages/messages.service.js
async find(params) {
  const { user } = params.account || {};
  // Vulnerable: if client can override the query, they may omit user filter
  const query = params.query || {};
  const snapshot = await db.collection('messages').where('ownerId', '==', user).get();
  const rows = [];
  snapshot.forEach((doc) => rows.push({ id: doc.id, ...doc.data() }));
  return { total: rows.length, data: rows };
}

async patch(id, data, params) {
  // Vulnerable: id is used directly without verifying ownership
  await db.collection('messages').doc(id).update(data);
  return this.get(id, params);
}

When combined with Firestore’s flexible security rules, developers might assume rules alone prevent IDOR, but rules are not a substitute for application-level authorization in Feathers. Rules can be misconfigured or bypassed if the service layer does not enforce ownership. For example, a Firestore rule like allow read: if request.auth != null; permits any authenticated user to read any document in the collection, which is too broad. The service must scope reads to documents owned by the requesting user, typically by injecting the user ID into the query and validating it matches the requested resource.

Additionally, IDOR risk increases when Firestore references are exposed in API responses or URLs. If an endpoint returns a document ID that should be opaque to the client, or if the client can manipulate that ID to traverse relationships (e.g., changing conversationId in a nested path), the attack surface expands. Feathers middleware that does not sanitize or validate IDs before they reach Firestore queries can unintentionally facilitate horizontal privilege escalation.

To detect IDOR in a Feathers + Firestore stack, a scanner like middleBrick runs unauthenticated and authenticated checks, attempting to access resources using known IDs that belong to other users. It observes whether the service enforces ownership and whether Firestore rules inadvertently permit cross-user access. Findings include missing user scoping, overly permissive rules, and unsafe usage of IDs directly from request parameters.

Firestore-Specific Remediation in Feathersjs — concrete code fixes

Remediation centers on ensuring every Firestore operation is scoped to the requesting subject and that IDs are never trusted from the client. Below are concrete, Firestore-aware fixes for the Feathers services shown earlier.

1. Scoped get with ownership check

Modify the service to require the user ID to be available in params.account (typically set by an authentication hook) and to build a query that includes both the document ID and the user ID. This ensures the requested document belongs to the requester.

// services/profiles/profiles.service.js
async get(id, params) {
  const { user } = params.account || {};
  if (!user) {
    throw new Error('Unauthenticated');
  }
  // Scoped read: verify ownership
  const doc = await db.collection('profiles')
    .doc(id)
    .where('userId', '==', user)
    .limit(1)
    .get();
  // Since we query by ID and userId, we can check the query result
  // Note: Firestore does not support get by doc ID + field filter directly;
  // instead, fetch the doc and validate field.
  const snap = await db.collection('profiles').doc(id).get();
  if (!snap.exists) {
    throw new Error('Not found');
  }
  if (snap.get('userId') !== user) {
    throw new Error('Forbidden');
  }
  return snap.data();
}

A more idiomatic approach is to rely on Firestore security rules for read validation and keep the service simple, but you must still ensure the service does not leak IDs. Alternatively, use a map of allowed IDs per user stored server-side (e.g., in a mapping collection) and check membership before proceeding.

2. Safe find with strict ownership filtering

Ensure the query always filters by the authenticated user and never allows the client to override the filter. Remove any user-supplied query fields that could affect the ownership filter.

// services/messages/messages.service.js
async find(params) {
  const { user } = params.account || {};
  if (!user) {
    throw new Error('Unauthenticated');
  }
  // Enforce server-side filter; ignore any client-supplied where clauses
  const snapshot = await db.collection('messages')
    .where('ownerId', '==', user)
    .get();
  const rows = [];
  snapshot.forEach((doc) => rows.push({ id: doc.id, ...doc.data() }));
  return { total: rows.length, data: rows };
}

3. Secure patch with ownership verification

Before updating, confirm the document exists and belongs to the user. Avoid using the client-supplied ID alone; couple it with a user check.

// services/messages/messages.service.js
async patch(id, data, params) {
  const { user } = params.account || {};
  if (!user) {
    throw new Error('Unauthenticated');
  }
  const docRef = db.collection('messages').doc(id);
  const snap = await docRef.get();
  if (!snap.exists) {
    throw new Error('Not found');
  }
  if (snap.get('ownerId') !== user) {
    throw new Error('Forbidden');
  }
  await docRef.update(data);
  return { id, ...data };
}

4. Use hooks to inject identity and sanitize input

Feathers hooks are the right place to normalize the user context and ensure every service has a reliable params.account. Also, sanitize IDs to prevent malformed references.

// app.hooks.js
const { iff, isProvider } = require('feathers-hooks-common');

module.exports = {
  before: {
    all: [],
    find: [iff(isProvider('external'), sanitizeQuery)],
    get: [verifyIdFormat],
    create: [stripUserOverride],
    update: [stripUserOverride],
    patch: [iff(isProvider('external'), verifyOwnershipPatch)],
    remove: [iff(isProvider('external'), verifyOwnershipPatch)]
  }
};

function verifyOwnershipPatch(hook) {
  const { user } = hook.params.account || {};
  if (!user) return Promise.reject(new Error('Unauthenticated'));
  const id = hook.id;
  // For patch, ensure the resource belongs to the user
  return hook.app.service('messages').get(id, hook.params)
    .then(data => {
      if (data.ownerId !== user) {
        throw new Error('Forbidden');
      }
      return hook;
    })
    .catch(err => Promise.reject(err));
}

function verifyIdFormat(hook) {
  const id = hook.id;
  if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(id)) {
    return Promise.reject(new Error('Bad request'));
  }
  return hook;
}

5. Security rules as a safety net, not the primary control

Configure Firestore rules to require authentication and to align with your data model, but do not rely on them for per-request ownership checks in Feathers. Example rule that requires user ID to match a field:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /messages/{messageId} {
      allow read, update: if request.auth != null && request.auth.uid == request.resource.data.ownerId;
      allow create: if request.auth != null && request.resource.data.ownerId == request.auth.uid;
    }
  }
}

Note: request.resource.data is only available on create/update; for reads you must use resource.data and ensure the document exists and matches rules. Rules should complement, not replace, service-layer checks.

6. Middleware and validation to prevent ID tampering

Use a hook to ensure IDs are not mangled or overridden in unexpected ways. For REST transports, validate path and query parameters. For sockets, validate the dispatched payload.

function sanitizeQuery(hook) {
  // Remove any user-supplied filter that could affect ownership
  delete hook.params.query.userId;
  delete hook.params.query.$limit;
  delete hook.params.query.$skip;
  // Keep paging controlled server-side
  hook.params.query.$limit = 50;
  return hook;
}

These patterns reduce the likelihood of IDOR by ensuring that every Firestore operation is explicitly scoped to the authenticated subject and that client-controlled identifiers are never used directly without verification.

Related CWEs: bolaAuthorization

CWE IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Can Firestore security rules alone prevent IDOR in a Feathersjs service?
No. Rules are a necessary layer but do not replace application-level ownership checks. A Feathers service must scope queries to the authenticated user; otherwise, an authenticated attacker can manipulate IDs to access other users' data even if rules permit reads in general.
How does middleBrick detect IDOR in Feathersjs + Firestore setups?
middleBrick runs authenticated checks that attempt to access resources using identifiers associated with other users. It observes whether the service enforces ownership before passing the ID to Firestore and whether queries are properly scoped. Findings highlight missing user scoping and overly permissive usage of direct IDs.