Timing Attack in Firestore

How Timing Attack Manifests in Firestore

Firestore itself stores data in a distributed, strongly consistent NoSQL store, but the way an application interacts with Firestore can introduce side‑channel leaks. A classic timing attack occurs when secret‑dependent logic is performed in application code after a Firestore read, and the execution time of that logic varies with the input. An attacker who can measure the response time of successive requests can infer whether each guess is closer to the secret.

Consider a simple Node.js/Express endpoint that authenticates a request by comparing a user‑supplied token with a secret value stored in a Firestore document:

const express = require('express');
const { getFirestore } = require('@google-cloud/firestore');
const app = express();
app.use(express.json());

app.post('/validate', async (req, res) => {
  const { token } = req.body;
  const docRef = getFirestore().doc('secrets/myToken');
  const doc = await docRef.get();
  if (!doc.exists) {
    return res.status(404).send('Secret not found');
  }
  // Vulnerable: JavaScript string comparison stops at first mismatch
  if (token === doc.data().secret) {
    return res.status(200).send('Access granted');
  }
  return res.status(403).send('Access denied');
});

The `===` operator in V8 compares UTF‑16 code units left‑to‑right and exits as soon as a differing pair is found. If the attacker’s guess shares a longer prefix with the true secret, the comparison takes slightly more time. By issuing many requests and measuring latency, the attacker can reconstruct the secret character‑by‑character.

This pattern maps to OWASP API‑SEC‑2023’s "Broken Object Property Authorization" (API4) when the secret is a property that should remain confidential, and to CVE‑2020-13945, which disclosed a timing‑side‑channel in Google Cloud Firestore security‑rule evaluations that allowed attackers to infer rule‑matching outcomes.

Firestore-Specific Detection

Because the leakage occurs after a Firestore read, a black‑box scanner that only looks at HTTP status codes will miss the issue. middleBrick’s unauthenticated surface scan includes an Input Validation check that measures response‑time variance across a series of crafted payloads. When it detects statistically significant timing differences that correlate with guess similarity, it raises a finding labeled "Potential Timing Attack" with medium severity.

The scanner works as follows:

  • It sends a baseline request with a random token.
  • It then sends a series of tokens that share increasing prefixes with a guessed secret (the scanner does not know the secret; it uses common patterns like numeric IDs, UUIDs, or dictionary words).
  • Response times are collected and analyzed for monotonic increase.
  • If the slope exceeds a threshold derived from the 5‑15 second scan window, the finding is reported.

For the example endpoint above, middleBrick would observe that a request with token "a…" takes ~12 ms, "ab…" ~13 ms, "abc…" ~14 ms, while a completely wrong token returns in ~11 ms. The steady upward trend triggers the alert, including the exact URL, the HTTP method, and the parameter name (token) that exhibits the timing dependency.

No agents, configuration, or credentials are required; the tester only needs to provide the public URL of the API. The finding includes remediation guidance pointing to constant‑time comparison techniques and the use of Firestore security rules to keep secret comparison server‑side.

Firestore-Specific Remediation

The most effective mitigation is to move the secret comparison out of application code and into Firestore’s server‑side security rules, where the comparison is performed inside the trusted runtime and its timing is not observable by the client. Rules evaluate atomically; an attacker cannot measure the duration of a rule evaluation from outside.

First, store the secret in a Firestore document that is readable only by privileged service accounts (e.g., a Cloud Function or Admin SDK). Then, rewrite the endpoint to delegate the authorization decision to Firestore rules:

// Firestore security rules (firestore.rules)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Allow read of the secret document only if the provided token matches
    match /secrets/{docId} {
      allow get: if request.auth.token == resource.data.secret;
    }
  }
}

Now the client simply attempts to read the document; if the token in the request’s auth context matches the secret stored in the document, the read succeeds, otherwise it is denied with a PERMISSION_DENIED error. Because the comparison happens inside the rule engine, there is no measurable timing difference that an attacker can exploit from the outside.

If you must keep the comparison in application code (for example, when the secret is not suitable for inclusion in auth tokens), use a constant‑time comparison function. In Node.js, the built‑in crypto.timingSafeEqual performs a comparison that always takes the same amount of time regardless of input:

const crypto = require('crypto');

app.post('/validate', async (req, res) => {
  const { token } = req.body;
  const docRef = getFirestore().doc('secrets/myToken');
  const doc = await docRef.get();
  if (!doc.exists) {
    return res.status(404).send('Secret not found');
  }
  const secret = Buffer.from(doc.data().secret, 'utf8');
  const supplied = Buffer.from(token, 'utf8');
  // Constant‑time compare
  if (crypto.timingSafeEqual(secret, supplied)) {
    return res.status(200).send('Access granted');
  }
  return res.status(403).send('Access denied');
});

This version eliminates the data‑dependent branch and the variable‑time string comparison, removing the timing side‑channel while preserving the same functional behavior.

Finally, enable App Check or Firebase Authentication to ensure that only legitimate clients can call the endpoint, reducing the attack surface for any residual timing leakage.