MEDIUM side channel attackfirestore

Side Channel Attack in Firestore

How Side Channel Attack Manifests in Firestore

Firestore’s security model relies on declarative rules that evaluate each read, write, or delete request. When those rules inadvertently leak information through observable characteristics — such as response latency, error messages, or the presence/absence of a document — an attacker can infer sensitive data without breaking the rules themselves. This class of attack is known as a side‑channel attack.

Two common Firestore‑specific side‑channel vectors are:

  • Timing‑based existence inference: A rule that performs a conditional get() on a secret document only when a certain field matches a guess will take measurably longer when the guess is correct because the extra read is executed. By measuring the round‑trip time of many requests, an attacker can binary‑search for the value of a hidden field (e.g., a user’s role or a payment amount).
  • Size‑based data leakage via query result counts: Firestore charges for the number of documents returned. If a rule allows a query that returns a count based on a secret field (for example, where('status', '==', request.resource.data.secret)), an attacker can observe the billing‑reported read count or the latency difference caused by index scanning to deduce the secret’s value.
  • Consider the following vulnerable rule set that exposes a user’s accountTier field through timing:

    // firestore.rules
    service cloud.firestore {
      match /databases/{database}/documents {
        match /users/{userId} {
          allow read: if request.auth.uid == userId &&
                      (get(/databases/$(database)/documents/accounts/$(request.auth.uid)).data.tier == 'premium' ||
                       true); // the extra get() runs only for premium users
        }
      }
    }
    

    When a non‑premium user makes a read, the rule skips the get() and returns quickly. A premium user triggers an additional document read, adding roughly 10‑30 ms of latency (depending on region and cache state). An attacker who can issue many requests and measure response times can distinguish premium from non‑premium accounts, thereby learning the tier field without ever being granted direct read access.

    A second example uses query‑count side channels:

    // firestore.rules
    match /transactions/{txId} {
      allow read: if request.auth != null &&
                    request.resource.data.amount > 0 &&
                    (get(/databases/$(database)/documents/ledger/$(request.auth.uid)).data.balance > 0);
    }
    

    If the ledger document does not exist or has a zero balance, the rule fails early and returns a permission denied error instantly. When the balance is positive, the rule proceeds to evaluate the rest of the expression, which may involve additional index look‑ups, causing a measurable delay. By repeatedly querying with crafted amount values and timing the responses, an attacker can infer whether the balance is above zero.

Firestore-Specific Detection

Detecting these side‑channel patterns requires observing behavior that is not captured by traditional vulnerability scanners focused on injection or broken authentication. The key indicators are:

  • Conditional rule logic that invokes additional get(), exists(), or getAfter() calls based on user‑controlled data.
  • Rules that compare a request field against a secret field stored in another document, causing divergent execution paths.
  • Queries whose cost (number of documents read, index usage, or latency) varies with the value of a secret field.

middleBrick’s black‑box scanner can surface these issues by probing the unauthenticated Firestore REST endpoint (or the SDK‑generated gRPC calls) and measuring response times under different inputs. Because middleBrick runs 12 parallel security checks — including Input Validation and Property Authorization — it will flag rules where:

  • A read request with parameter X=guess1 yields a significantly different response time than X=guess2 (statistical timing analysis).
  • The number of documents returned or the reported read cost changes when the guess toggles a secret condition.

Example detection workflow:

  1. Submit the Firestore endpoint URL to middleBrick (via Dashboard, CLI, or GitHub Action).
  2. middleBrick performs a baseline scan, then sends a series of crafted requests that vary a single query parameter (e.g., ?minAmount=100 vs ?minAmount=200).
  3. It measures latency and read‑count metrics for each variant.
  4. If the variance exceeds a configurable threshold (derived from baseline jitter), middleBrick raises a finding titled "Potential timing side‑channel in Firestore security rules" with severity Medium and points to the exact rule line.

Because middleBrick does not require agents or credentials, the test can be run against a production‑like staging URL to catch regressions before they reach live environments. The finding includes remediation guidance that references Firestore‑specific best practices (see next section).

Firestore-Specific Remediation

The goal is to eliminate data‑dependent branching in security rules and to ensure that any necessary checks perform in constant time with respect to secret data. Firestore provides several language features that help achieve this:

  • Avoid conditional reads: Pre‑fetch all needed data in a single get() call outside of conditional branches, or restructure the rule so that the decision does not depend on the result of a read.
  • Use exists() for presence checks: When you only need to know whether a document exists, exists() is cheaper and does not return fields that could leak timing differences.
  • Leverage request.time for rate‑based mitigations: If you must delay an attacker, add a uniform sleep()-like behavior via request.time comparisons that apply equally to all users.
  • Apply field masking: Never return secret fields in rules that are used for read authorization; instead, store a derived, non‑sensitive flag (e.g., canViewPremium) that is updated via a trusted backend.

Revised rule set that removes the timing side‑channel from the first example:

// firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // Fetch the tier once, outside any conditional
      let tier = get(/databases/$(database)/documents/accounts/$(userId)).data.tier;
      allow read: if request.auth.uid == userId &&
                  (tier == 'premium' || true); // tier is now a constant for this request
    }
  }
}

Because the get() executes unconditionally for every request, the latency no longer correlates with the tier value. The rule still enforces the same business logic (premium users get an extra check) but does so in constant time.

For the query‑count side channel, replace the conditional ledger check with a constant‑time flag:

// firestore.rules
match /transactions/{txId} {
  allow read: if request.auth != null &&
                request.resource.data.amount > 0 &&
                get(/databases/$(database)/documents/accounts/$(request.auth.uid)).data.canSpend;
}

Here, canSpend is a Boolean maintained by a trusted Cloud Function that updates whenever the ledger balance changes. The rule now performs a single read whose result does not depend on the secret balance value, eliminating the timing variance.

Client‑side code should also avoid inadvertently revealing secrets through error messages. Use the Firebase SDK’s error handling to present generic messages:

// JavaScript (Web)
import { getFirestore, doc, getDoc } from 'firebase/firestore';
const db = getFirestore();
async function getUserProfile(uid) {
  try {
    const snap = await getDoc(doc(db, 'users', uid));
    if (!snap.exists()) throw new Error('User not found');
    return snap.data();
  } catch (e) {
    // Log the full error server‑side, but return a generic message to the client
    console.error('Firestore error:', e);
    throw new Error('Unable to retrieve profile');
  }
}

By combining rule‑level constant‑time patterns with disciplined client error handling, you remove the observable side‑channels that attackers could exploit to infer sensitive Firestore data.

Frequently Asked Questions

Can middleBrick detect side‑channel vulnerabilities in Firestore without accessing my project’s credentials?
Yes. middleBrick performs unauthenticated, black‑box checks against the public Firestore endpoint (REST or gRPC). It sends varied requests and measures response latency and read‑count differences to infer timing‑ or size‑based side channels, all without needing any API keys or service accounts.
If middleBrick flags a potential timing side channel, does it automatically fix my Firestore rules?
No. middleBrick only detects and reports the issue, providing detailed findings and remediation guidance. You must apply the recommended rule changes yourself — for example, moving conditional get() calls outside of branches or using constant‑time flags — to eliminate the vulnerability.