Skip to content

Firebase Security Vulnerability: FBR-PERF-001

Name: Excessive Reads in Rules (get/exists)

Applicable Services: Cloud Firestore

Description

Note

This performance vulnerability occurs when security rules for list (query) operations contain get() or exists() calls. This forces Firestore to perform an additional document read for every single document the query considers, leading to excessive costs and failed requests.

  • The Technical Flaw: A rule intended to secure a collection query (e.g., allow list: if ...) contains a get() or exists() call. When a client queries this collection, the rule is evaluated against every document. This creates a "fan-out" effect where one client query triggers N dependent reads within Firestore's backend.
  • Intended Behavior: Rules should be efficient. For permission checks on queries, the best practice is to use data that doesn't require extra reads, such as checking properties on the queried document itself or using Firebase Authentication Custom Claims, which are embedded in the user's auth token.
  • The Risk: This pattern can lead to unexpectedly high Firestore bills and a self-inflicted Denial of Service (DoS). A query on a large collection can easily exceed the 10-document-access-per-evaluation limit, causing the request to fail entirely.

Impact

Potential Consequences

The severity of this vulnerability is a warning. Exploiting it can lead to: * Excessive Costs: A single query can result in thousands of document reads, leading to a massive, unexpected increase in your Firebase bill. * Denial of Service (DoS): Queries on even moderately sized collections will fail because they exceed Firestore's internal limits for rule evaluation, making parts of your application unusable. * Poor Performance: Requests that don't fail will be significantly slower due to the extra read latency for every document checked.

Example Attack Scenarios

Exploit Scenario: N+1 Read Problem in a List Query

  • The Vulnerable Rule: The following Firestore rule tries to grant list access to a projects collection only to users who are members of that project. It does this by checking a members subcollection for each project using exists().
    // VULNERABLE: This rule causes an N+1 read problem.
    match /projects/{projectId} {
      // A user can list projects if they are a member.
      allow list: if exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid));
      // Read/write rules for individual documents are fine.
      allow read, write: if get(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid)).data.role in
    }
    
  • Attacker's Goal: This is not a malicious attack but a flawed implementation. A developer wants to display a list of all projects to the user.
  • The Exploit: The developer writes a standard client-side query to fetch all documents in the projects collection.
    // This simple query will trigger the vulnerability.
    // If there are 500 projects, this will result in 501 reads.
    db.collection("projects").get()
      .then((querySnapshot) => {
        console.log("Successfully listed projects");
      })
      .catch((error) => {
        // This error will likely occur due to exceeding read limits.
        console.error("Error listing projects: ", error);
      });
    
  • Outcome: The user's query on the projects collection forces the rule to execute an exists() check for every single project document in the database. If there are 100 projects, this one query results in 101 reads. If there are 1000, it results in 1001 reads and will almost certainly fail for exceeding Firestore's limits, causing a Denial of Service.

Mitigation

The vulnerability is resolved by removing the get() or exists() call from the list rule. The best practice is to use Firebase Authentication Custom Claims for role-based access or to denormalize the data so the check can be performed on the document itself.

// VULNERABLE: The `exists()` call is evaluated for every document.
match /projects/{projectId} {
  allow list: if exists(/databases/$(database)/documents/projects/$(projectId)/members/$(request.auth.uid));
}
// SECURE: This rule uses a denormalized list of member UIDs
// stored directly on the project document. No extra reads are needed.
match /projects/{projectId} {
  // The user's UID must be in the `members` array on the project document.
  allow list: if request.auth.uid in resource.data.members;
}

Best Practice: Use Custom Claims for Roles

An even better solution for role-based access is to use Firebase Authentication Custom Claims. You can set a claim like {isAdmin: true} on a user's token via a Cloud Function. The rule is then simple, fast, and requires no extra reads:

// IDEAL: Checks a custom claim on the user's auth token.
// This is the most performant and secure method for role-based access.
match /projects/{projectId} {
  allow list: if request.auth.token.isAdmin == true;
}

References