Skip to content

Firebase Security Vulnerability: FBR-ACCESS-003

Name: Firestore Overly Broad Recursive Wildcard Access Vulnerability

Applicable Services: Cloud Firestore

Description

Technical Vulnerability Overview

The Technical Flaw: Recursive wildcard patterns ({doc=**}) combined with simple authorization conditions grant access to all nested documents and subcollections under a given path, including sensitive data not intended for direct client access.

Intended Behavior: Access controls should be granular and specific, granting access only to the exact collections and documents that users legitimately need to access, not broad swaths of nested data.

The Risk: Users can access sensitive subcollections containing private settings, logs, administrative data, or other confidential information that was never intended to be directly accessible by client applications.

Impact

Potential Consequences

This vulnerability can lead to significant data exposure and privacy violations:

  • Sensitive Data Exposure: Access to private settings, configuration data, or administrative information stored in nested subcollections
  • Privacy Violations: Exposure of user logs, activity tracking, or behavioral analytics data not intended for client access
  • Administrative Data Breach: Access to system logs, debug information, or internal application metadata
  • Audit Trail Compromise: Unauthorized access to security logs, transaction histories, or compliance records
  • Internal Configuration Exposure: Access to API keys, feature flags, or other sensitive configuration data stored in nested collections
  • Privilege Escalation: Discovery of hidden admin interfaces or elevated permission data through subcollection enumeration

Example Attack Scenarios

Exploit Scenario: Private Settings and Configuration Data Access

The Vulnerable Rule:

match /users/{userId}/{doc=**} {
  allow read: if request.auth.uid == userId;
}

Attacker's Goal: To access sensitive configuration data and private settings stored in nested subcollections.

The Exploit: A user discovers they can access far more than intended through the wildcard rule:

// User expects to access their profile
const userProfile = await db.collection('users').doc(currentUser.uid).get();

// But discovers they can access private settings
const privateSettings = await db.collection('users').doc(currentUser.uid)
                          .collection('private_settings').doc('config').get();

console.log('Private settings exposed:', privateSettings.data());
// Output: { apiKeys: {...}, internalFlags: {...}, debugMode: true }

// Access administrative logs
const adminLogs = await db.collection('users').doc(currentUser.uid)
                        .collection('admin_logs').get();

adminLogs.docs.forEach(log => {
  console.log('Admin log entry:', log.data());
  // Exposes internal system operations, error details, etc.
});

// Access audit trail
const auditTrail = await db.collection('users').doc(currentUser.uid)
                         .collection('audit').get();

auditTrail.docs.forEach(entry => {
  console.log('Audit entry:', entry.data());
  // Exposes security events, access patterns, etc.
});

Outcome: The user gains access to sensitive configuration data, administrative logs, and audit information that should be restricted to server-side access only.

Exploit Scenario: System Analytics and Internal Data Harvesting

The Vulnerable Rule:

match /organizations/{orgId}/{doc=**} {
  allow read: if request.auth.uid in resource.data.members;
}

Attacker's Goal: To harvest internal analytics, usage data, and business intelligence from nested subcollections.

The Exploit: An organization member exploits the wildcard to access sensitive business data:

// Normal access to organization info
const orgInfo = await db.collection('organizations').doc('company-123').get();

// Discover and access analytics subcollections
const analyticsCollections = [
  'analytics/revenue/monthly',
  'analytics/users/demographics', 
  'analytics/performance/metrics',
  'internal/financial_reports',
  'internal/employee_data',
  'logs/security_events',
  'logs/api_usage'
];

const harvestedData = {};

for (const path of analyticsCollections) {
  try {
    const pathParts = path.split('/');
    let ref = db.collection('organizations').doc('company-123');

    for (let i = 0; i < pathParts.length - 1; i += 2) {
      ref = ref.collection(pathParts[i]);
      if (pathParts[i + 1]) {
        ref = ref.doc(pathParts[i + 1]);
      }
    }

    const data = await ref.get();
    if (data.exists) {
      harvestedData[path] = data.data();
      console.log(`Harvested ${path}:`, data.data());
    }
  } catch (error) {
    // Continue to next path
  }
}

// Export sensitive business intelligence
const businessData = {
  revenue: harvestedData['analytics/revenue/monthly'],
  userDemographics: harvestedData['analytics/users/demographics'],
  employeeInfo: harvestedData['internal/employee_data'],
  securityEvents: harvestedData['logs/security_events']
};

console.log('Harvested business intelligence:', businessData);

Outcome: The attacker obtains comprehensive business intelligence including revenue data, user analytics, employee information, and security logs that should be restricted to management access only.

Mitigation

The vulnerability is resolved by replacing broad wildcard patterns with specific, granular path matching and implementing explicit access controls for each collection.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // VULNERABLE: Overly broad wildcard grants access to all nested data
    match /users/{userId}/{doc=**} {
      allow read: if request.auth.uid == userId;
    }

    // VULNERABLE: Organization wildcard exposes internal data
    match /organizations/{orgId}/{doc=**} {
      allow read: if request.auth.uid in resource.data.members;
    }

    // VULNERABLE: Admin wildcard with weak condition
    match /admin/{section}/{doc=**} {
      allow read: if request.auth.token.admin == true;
    }
  }
}
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // SECURE: Specific path matching for intended user data
    match /users/{userId} {
      allow read, write: if request.auth.uid == userId;
    }

    match /users/{userId}/profile/{profileDoc} {
      allow read, write: if request.auth.uid == userId;
    }

    match /users/{userId}/posts/{postId} {
      allow read, write: if request.auth.uid == userId;
    }

    match /users/{userId}/preferences/{prefDoc} {
      allow read, write: if request.auth.uid == userId;
    }

    // SECURE: Explicitly deny access to sensitive subcollections
    match /users/{userId}/private_settings/{doc=**} {
      allow read, write: if false; // Server-side only
    }

    match /users/{userId}/admin_logs/{doc=**} {
      allow read, write: if false; // Server-side only
    }

    match /users/{userId}/audit/{doc=**} {
      allow read, write: if false; // Server-side only
    }

    // SECURE: Organization access with granular permissions
    match /organizations/{orgId} {
      allow read: if request.auth.uid in resource.data.members;
    }

    match /organizations/{orgId}/public_data/{docId} {
      allow read: if request.auth.uid in get(/databases/$(database)/documents/organizations/$(orgId)).data.members;
    }

    // SECURE: Admin access to specific collections only
    match /organizations/{orgId}/analytics/{doc=**} {
      allow read: if request.auth.token.role == 'admin' && 
                 request.auth.uid in get(/databases/$(database)/documents/organizations/$(orgId)).data.admins;
    }

    match /organizations/{orgId}/internal/{doc=**} {
      allow read, write: if false; // Server-side only
    }

    // SECURE: Conditional wildcard with strong validation
    match /shared_documents/{userId}/{doc=**} {
      allow read: if request.auth != null && 
                 (request.auth.uid == userId ||
                  request.auth.uid in resource.data.sharedWith) &&
                 resource.data.visibility == 'shared';
    }
  }
}

Wildcard Security Best Practices

Avoid Recursive Wildcards: Replace {doc=**} patterns with specific collection paths whenever possible to maintain granular access control.

Principle of Least Privilege: Grant access only to the specific collections and documents users legitimately need, not broad categories of data.

Explicit Denial for Sensitive Data: Use allow read, write: if false; rules to explicitly deny access to sensitive subcollections.

Strong Conditions for Necessary Wildcards: When wildcards are unavoidable, implement multiple validation checks:

match /user_files/{userId}/{doc=**} {
  allow read: if request.auth != null && 
             request.auth.uid == userId &&
             resource.data.fileType == 'public' &&
             resource.data.approved == true;
}

Collection-Specific Rules: Define separate rules for each subcollection to maintain clear access boundaries: - Public data: Standard user access - Private settings: Server-side only - Logs: Admin access with time restrictions - Analytics: Role-based access with data filtering

Testing Wildcard Rules: Always test with Firebase Rules Simulator to verify that wildcards don't grant unintended access to sensitive data.

References