HIGH insecure designexpressfirestore

Insecure Design in Express with Firestore

Insecure Design in Express with Firestore — how this specific combination creates or exposes the vulnerability

Insecure design in an Express service that uses Cloud Firestore often stems from trusting the client to supply document identifiers or paths and then constructing Firestore references without validating authorization context. Unlike SQL databases where row-level security can be enforced with a single parameterized query, Firestore relies on the application to enforce access rules on document reads and writes. When Express routes directly use user-supplied IDs to build docRef = db.collection('orders').doc(req.body.orderId), the server may assume the caller can only access their own data, but the Firestore security rules may not enforce this constraint tightly, or the server-side authorization check may be incomplete.

An insecure design pattern is to perform a per-request authorization check by reading the document with the supplied ID and then deciding whether to proceed based on the returned document contents. This introduces a time-of-check to time-of-use (TOCTOU) issue: the document could be deleted or its ownership could change between the read and the subsequent write, leading to unauthorized operations. Additionally, if the Express app uses the Admin SDK with elevated privileges, a flawed route that trusts client input can lead to mass data access or modification, because the Admin SDK bypasses Firestore security rules.

Consider an endpoint that accepts a projectId to list tasks:

// Insecure route example
app.get('/projects/:projectId/tasks', async (req, res) => {
  const { projectId } = req.params;
  const tasksSnapshot = await db.collection('projects').doc(projectId).collection('tasks').get();
  res.json(tasksSnapshot.docs.map(d => d.data()));
});

If the route does not validate that the authenticated user belongs to the specified projectId, an attacker can enumerate or access tasks from any project by guessing or iterating IDs. This is a BOLA/IDOR design flaw: the design assumes path-based scoping is sufficient, but authorization is missing. Firestore’s indexing and flexible schema can make it easy to accidentally expose related collections when the authorization boundary is not enforced consistently in both rules and application logic.

Another insecure design involves constructing Firestore queries that rely on client-supplied field values without strict validation. For example, allowing a client to specify a filter on a query without server-side constraints can lead to data exposure or inefficient queries that degrade performance. Insecure deserialization of Firestore documents on the server can also introduce injection-like risks if the application directly uses raw client input to shape queries or document updates without whitelisting fields.

Compliance mappings such as OWASP API Top 10 (2023) highlight this as Broken Object Level Authorization (BOLA), which is commonly found in API designs that do not centralize authorization logic. PCI-DSS, SOC 2, and GDPR further require that access to personal or payment data be scoped to the minimum necessary context, which is violated when Firestore document access is not tightly coupled with the authenticated subject’s permissions.

Firestore-Specific Remediation in Express — concrete code fixes

To remediate insecure design when using Express with Firestore, enforce authorization on every request by resolving the authenticated subject and scoping Firestore queries and document references accordingly. Avoid using client-supplied identifiers directly as document IDs without mapping them to the authenticated user’s allowed resources.

First, map the authenticated user to their allowed project IDs, for example via a user metadata collection or an access control list, and use that mapping to scope reads and writes:

// Secure route with explicit authorization
app.get('/projects/:projectId/tasks', async (req, res) => {
  const { projectId } = req.params;
  const userId = req.user.id; // authenticated subject from middleware

  // Verify user has access to this project
  const userProjectRef = db.collection('userProjects').doc(userId).collection('projects').doc(projectId);
  const projectDoc = await userProjectRef.get();
  if (!projectDoc.exists) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const tasksSnapshot = await db.collection('projects').doc(projectId).collection('tasks').get();
  res.json(tasksSnapshot.docs.map(d => d.data()));
});

This pattern ensures that access is verified per request and avoids TOCTOU by not relying on document content alone to determine authorization. The check against a dedicated membership collection makes the boundary explicit and aligns with least-privilege principles.

When writing data, use the same scoping to prevent unauthorized updates. Instead of allowing clients to specify arbitrary document IDs, derive the document reference from the authenticated subject:

// Secure write example
app.post('/projects/:projectId/tasks', async (req, res) => {
  const { projectId } = req.params;
  const userId = req.user.id;

  // Ensure user can write to this project
  const userProjectRef = db.collection('userProjects').doc(userId).collection('projects').doc(projectId);
  const projectDoc = await userProjectRef.get();
  if (!projectDoc.exists) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const taskRef = db.collection('projects').doc(projectId).collection('tasks').doc();
  await taskRef.set({
    ...req.body,
    createdBy: userId,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });
  res.status(201).json({ id: taskRef.id });
});

If you must accept a document ID from the client, resolve it through a mapping collection rather than using it directly as a path component. This prevents attackers from traversing collections unintentionally:

// Resolve via mapping instead of direct path
app.get('/user-tasks/:taskMappingId', async (req, res) => {
  const { taskMappingId } = req.params;
  const userId = req.user.id;

  const mappingRef = db.collection('userTaskMapping').doc(userId).collection('tasks').doc(taskMappingId);
  const mappingDoc = await mappingRef.get();
  if (!mappingDoc.exists) {
    return res.status(404).json({ error: 'Not found' });
  }

  const taskRef = db.collection('tasks').doc(mappingDoc.data().taskId);
  const taskDoc = await taskRef.get();
  res.json(taskDoc.data());
});

For continuous protection, integrate the middleBrick Pro plan to enable continuous monitoring of your API endpoints. The dashboard and GitHub Action integrations can alert you when new endpoints or changes introduce BOLA risks, helping you maintain secure authorization boundaries over time. These tools do not fix the logic but surface misconfigurations so you can apply the patterns above consistently.

Frequently Asked Questions

Why is checking document existence after a query not sufficient for authorization?
Because a time-of-check to time-of-use (TOCTOU) window exists between the read and any subsequent operation. An attacker can modify or delete the document or change their permissions between checks, making existence-based authorization unsafe without scoping to the authenticated subject.
How does Firestore’s flexible schema increase the risk of insecure design in Express APIs?
Flexible schemas allow client-supplied fields to influence queries and document structure without strict validation. Without server-side field whitelisting and strict query constraints, this can lead to unintended data exposure or injection-like behaviors that violate authorization boundaries.