HIGH broken access controlexpressfirestore

Broken Access Control in Express with Firestore

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

Broken Access Control occurs when Express APIs fail to enforce proper authorization checks between authenticated users and Firestore resources. In a typical setup, Express routes authenticate a user (often via JWT or session) but then construct Firestore queries using only the authenticated UID — for example, querying users/{uid} or userPosts/{uid}/{postId} without verifying that the requesting user is the resource owner or has explicit permissions. Because Firestore security rules are not a substitute for application-level authorization in Express, developers may mistakenly rely on rules alone while allowing broad read or write access from the backend. This becomes critical when route parameters are used directly to build Firestore paths without validation, enabling an authenticated user to iterate over other users’ IDs and access or modify data they should not see.

Consider an Express endpoint GET /api/posts/:postId that first authenticates the user and then retrieves the post from Firestore using db.collection('posts').doc(postId).get(). If the route does not verify that the authenticated user has access to that specific post — for example, by checking post visibility, team membership, or ownership — an attacker can enumerate valid post IDs (a BOLA/IDOR pattern) and read data belonging to others. Similarly, endpoints accepting user-supplied filters or query parameters can lead to insecure queries where a malicious user manipulates the query to access documents outside their permitted scope. Because Firestore indexes and query patterns are often predictable, this exposes data through mass assignment or insecure direct object references when Express does not enforce per-request authorization aligned with the principle of least privilege.

Another scenario involves privilege escalation through Firestore field-level permissions. An Express route might update a user profile with db.collection('users').doc(uid).update(req.body), but if the route merges req.body without strict whitelisting, an attacker can inject fields such as isAdmin: true or alter roles. Without explicit checks that the authenticated user is allowed to modify the target fields, Firestore will apply the update as the backend user (which often has elevated privileges), effectively bypassing intended access boundaries. This form of Broken Access Control is exacerbated when Express services use shared service accounts or administrative SDK credentials, as any compromised route can lead to widespread data exposure or unauthorized operations across the Firestore database.

Firestore-Specific Remediation in Express — concrete code fixes

To remediate Broken Access Control in Express with Firestore, enforce explicit per-request authorization that validates the authenticated subject against the target document and its fields. Always resolve document paths using the authenticated UID rather than client-supplied identifiers alone, and apply strict field-level filtering for updates. Below are concrete, secure patterns you can adopt.

1. Enforce ownership with UID-based paths and checks

For user-specific data such as posts or profiles, derive document references from the authenticated UID and verify ownership before reads or writes.

const { auth } = require('express-openid-connect');
const { getAuth } = require('firebase-admin/auth');
const db = require('firebase-admin').firestore();

app.get('/api/users/me/posts/:postId', async (req, res) => {
  const uid = req.oidc.user.sub; // authenticated subject from middleware
  const postId = req.params.postId;

  // Use UID to build canonical path; avoid trusting postId alone for access decisions
  const postRef = db.collection('users').doc(uid).collection('posts').doc(postId);
  const doc = await postRef.get();

  if (!doc.exists) {
    return res.status(404).json({ error: 'Not found' });
  }
  res.json(doc.data());
});

2. Authorize cross-owning resources with explicit checks

When a user may access resources owned by others (e.g., team posts), perform explicit checks against Firestore documents or a permissions collection before proceeding.

app.get('/api/teams/:teamId/posts/:postId', async (req, res) => {
  const uid = req.oidc.user.sub;
  const { teamId, postId } = req.params;

  const teamRef = db.collection('teams').doc(teamId);
  const teamSnap = await teamRef.get();

  if (!teamSnap.exists) {
    return res.status(404).json({ error: 'Team not found' });
  }

  const membershipRef = teamRef.collection('members').doc(uid);
  const membershipSnap = await membershipRef.get();

  if (!membershipSnap.exists) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const postRef = teamRef.collection('posts').doc(postId);
  const postSnap = await postRef.get();

  if (!postSnap.exists) {
    return res.status(404).json({ error: 'Post not found' });
  }

  res.json(postSnap.data());
});

3. Secure updates with field whitelisting and ownership validation

For updates, avoid merging raw req.body. Instead, validate the target document, confirm ownership or explicit write rights, and apply only allowed fields.

const allowedProfileFields = ['displayName', 'emailPreference'];

app.patch('/api/users/me', async (req, res) => {
  const uid = req.oidc.user.sub;
  const userRef = db.collection('users').doc(uid);

  // Ensure the document exists and belongs to the caller
  const snap = await userRef.get();
  if (!snap.exists) {
    return res.status(404).json({ error: 'Not found' });
  }

  // Whitelist permissible fields
  const updateData = {};
  allowedProfileFields.forEach((field) => {
    if (req.body[field] !== undefined) {
      updateData[field] = req.body[field];
    }
  });

  await userRef.update(updateData);
  res.json({ success: true });
});

4. Validate query inputs and avoid client-controlled collection/document traversal

Do not directly use client input to traverse collections in a way that bypasses ownership logic. Always anchor queries under a user or team root when possible.

app.get('/api/search/posts', async (req, res) => {
  const uid = req.oidc.user.sub;
  const { q } = req.query;

  // Search only within the user's accessible posts via a subcollection or team membership
  const userPostsRef = db.collection('users').doc(uid).collection('posts');
  const snapshot = await userPostsRef.where('title', '>=', q).where('title', '<=', q + '\uf8ff').get();

  const results = snapshot.docs.map((d) => ({ id: d.id, ...d.data() }));
  res.json({ results });
});

5. Use Firestore rules as a safety net, not the primary gate

Design Firestore rules to reject requests that lack ownership or explicit team membership, but ensure Express performs its own checks so that errors from rules do not leak sensitive information and to keep authorization logic testable in your application layer.

Mapping to security checks

middleBrick scans cover BOLA/IDOR, Property Authorization, and Unsafe Consumption among its 12 checks, which align with these patterns. If you use the middleBrick Pro plan, continuous monitoring can help detect regressions in authorization logic over time, while the GitHub Action can fail CI/CD builds when risk scores degrade.

Frequently Asked Questions

Can Firestore security rules alone prevent Broken Access Control in Express?
No. Firestore rules are a necessary layer but are not a substitute for explicit application-level authorization in Express. Rules cannot validate complex business logic such as team membership or multi-step workflows, and relying on them alone leaves room for IDOR and privilege escalation when Express routes directly expose document paths or merge unchecked client input.
How can I test whether my Express endpoints are vulnerable to IDOR with Firestore?
Use authenticated requests with different user identifiers and document IDs to verify that access is denied when the subject does not own or explicitly belong to the target. Tools like middleBrick can scan your endpoints for BOLA/IDOR findings; the free tier supports limited scans to help identify insecure direct object references in your API surface.