HIGH injection flawsfirestore

Injection Flaws in Firestore

How Injection Flaws Manifests in Firestore

Injection flaws in Firestore occur when untrusted data is improperly handled in database queries, allowing attackers to manipulate query logic and access unauthorized data. Unlike SQL injection, Firestore uses a document-oriented model with query builders, but the injection vectors are equally dangerous.

The most common Firestore injection pattern involves dynamic query construction. Consider this vulnerable code:

const userId = req.query.userId; // untrusted input
const snapshot = await db.collection('users')
  .where('id', '==', userId)
  .get();

If userId contains special characters like || or path traversal sequences, an attacker could craft queries that bypass authorization. Firestore's query language interprets certain characters and operators that can be exploited.

Another critical vector is collection name injection. When collection names are constructed dynamically:

const collectionName = req.query.collection;
const docId = req.query.id;
const doc = await db.collection(collectionName).doc(docId).get();

An attacker could supply collection=__meta__/documents to access Firestore's internal metadata collections, potentially exposing schema information or configuration data.

Firestore also has injection-like issues with field path resolution. When using update() or set() with dynamic field paths:

const fieldPath = req.body.field;
const value = req.body.value;
await db.collection('users').doc(userId).update({ [fieldPath]: value });

This allows attackers to update arbitrary fields, including nested fields they shouldn't access, by crafting malicious field path strings.

Document ID injection is another vector. When document IDs are constructed from user input:

const userId = req.body.userId;
const documentId = `user-${userId}`;
const doc = await db.collection('users').doc(documentId).get();

An attacker could use userId= to access the root document or use path traversal characters to access unintended documents.

Firestore's security rules can also be bypassed through injection when queries are constructed dynamically. If rules rely on query parameters that can be manipulated, attackers can craft queries that return unauthorized data even when rules appear correct.

Firestore-Specific Detection

Detecting injection flaws in Firestore requires both static code analysis and dynamic runtime testing. The key is identifying where untrusted data flows into query construction.

Static analysis should flag these patterns:

// Vulnerable: dynamic collection names
const col = db.collection(userInput);

// Vulnerable: dynamic field paths
const field = userInput;
doc.update({ [field]: value });

// Vulnerable: dynamic document IDs
const docId = userInput;
db.collection('users').doc(docId);

Dynamic detection with middleBrick scans APIs that interact with Firestore and tests for injection vulnerabilities by injecting special characters and path traversal sequences. The scanner tests:

  • Collection name manipulation with __meta__, __system__, and path traversal sequences
  • Field path injection with special characters like ., /, and array indices
  • Document ID manipulation including empty strings and special characters
  • Query parameter injection testing boolean operators and comparison operators

middleBrick's Firestore-specific checks include testing for BOLA (Broken Object Level Authorization) by attempting to access documents with manipulated IDs and testing privilege escalation by attempting to update fields that should be immutable.

Runtime monitoring should also watch for:

// Monitor for suspicious query patterns
const suspiciousQueries = [
  { pattern: /__meta__/i, reason: 'Metadata collection access' },
  { pattern: ///, reason: 'Path traversal' },
  { pattern: //, reason: 'Null byte injection' }
];

Security rules should be tested with automated tools that attempt to bypass them through query manipulation, ensuring that even if injection occurs, data remains protected.

Firestore-Specific Remediation

Remediating Firestore injection flaws requires a defense-in-depth approach combining input validation, query parameterization, and security rules.

First, implement strict input validation and whitelisting:

function validateCollectionName(name) {
  const validCollections = ['users', 'posts', 'comments'];
  if (!validCollections.includes(name)) {
    throw new Error('Invalid collection');
  }
  return name;
}

function validateUserId(id) {
  if (!/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
    throw new Error('Invalid user ID format');
  }
  return id;
}

Second, use parameterized queries where possible. While Firestore doesn't have traditional prepared statements, you can structure queries to avoid dynamic construction:

// Safe: predefined queries
const getUserById = async (db, userId) => {
  const snapshot = await db.collection('users')
    .where('id', '==', userId)
    .get();
  return snapshot.docs.map(doc => doc.data());
};

// Safe: validate before use
const safeGetDocument = async (db, collection, docId) => {
  const validatedCollection = validateCollectionName(collection);
  const validatedId = validateUserId(docId);
  const doc = await db.collection(validatedCollection).doc(validatedId).get();
  return doc.exists ? doc.data() : null;
};

Third, implement comprehensive security rules that don't rely on query parameters:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth.uid == userId;
    }
    
    match /posts/{postId} {
      allow read: if true;
      allow write: if request.auth.uid == request.resource.data.authorId;
    }
    
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Fourth, use Firestore's built-in validation features:

const validateDocument = (data, allowedFields) => {
  const unknownFields = Object.keys(data).filter(
    field => !allowedFields.includes(field)
  );
  if (unknownFields.length > 0) {
    throw new Error(`Unknown fields: ${unknownFields.join(', ')}`);
  }
};

// Usage
app.post('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  const userData = req.body;
  
  validateDocument(userData, ['name', 'email', 'age']);
  validateUserId(id);
  
  try {
    await db.collection('users').doc(id).set(userData);
    res.status(201).send();
  } catch (error) {
    res.status(400).send({ error: error.message });
  }
});

Finally, implement logging and monitoring for suspicious query patterns:

const suspiciousQueryPatterns = [
  /__meta__/,
  /\//,
  /\u0000/,
  /\.\./
];

const logSuspiciousQuery = (query, userId) => {
  const suspicious = suspiciousQueryPatterns.some(pattern => 
    pattern.test(query.toString())
  );
  
  if (suspicious) {
    console.warn(`Suspicious query detected`, {
      userId,
      query: query.toString(),
      timestamp: new Date().toISOString()
    });
  }
};

Frequently Asked Questions

Can Firestore's security rules alone prevent injection attacks?
No, security rules are essential but not sufficient. They protect data access but don't prevent the injection attacks that could cause denial of service, excessive billing, or information disclosure through error messages. You need both secure code patterns and proper security rules.
How does Firestore injection differ from SQL injection?
Firestore injection exploits document-oriented query construction rather than SQL syntax. Instead of SQL keywords, attackers manipulate collection names, document IDs, field paths, and query operators. The principles are similar—untrusted data in query construction—but the exploitation techniques differ due to Firestore's NoSQL structure.