TechLead
Lesson 8 of 8
6 min read
Firebase

Firebase Security Rules

Secure your database and storage with powerful, flexible security rules

What are Firebase Security Rules?

Firebase Security Rules provide a flexible, expression-based language to define who has read and write access to data stored in Firestore, Realtime Database, and Cloud Storage. Rules are evaluated on Firebase's servers, so they can't be bypassed by malicious clients.

Security Rules stand between your data and malicious users. They determine what data users can read and write, validate data structure, and enforce business logic—all without requiring a backend server.

🔒 Why Security Rules Matter

  • 🛡️ Data Protection: Prevent unauthorized access to sensitive data.
  • ✅ Data Validation: Ensure data meets your schema requirements.
  • 👤 User Isolation: Keep users' data private from other users.
  • 🚫 Rate Limiting: Prevent abuse with size and frequency limits.

Firestore Security Rules

Basic structure of Firestore rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Rules go here

    // Match a specific collection
    match /users/{userId} {
      // Allow read if user is authenticated
      allow read: if request.auth != null;

      // Allow write only to own document
      allow write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Common Firestore Rule Patterns

Practical examples for common scenarios:

User-owned Data

match /users/{userId} {
  // Users can only read/write their own data
  allow read, write: if request.auth != null && request.auth.uid == userId;
}

match /posts/{postId} {
  // Anyone can read posts
  allow read: if true;

  // Only the author can create/update/delete
  allow create: if request.auth != null
    && request.resource.data.authorId == request.auth.uid;

  allow update, delete: if request.auth != null
    && resource.data.authorId == request.auth.uid;
}

Role-based Access

match /admin/{document=**} {
  // Only allow access if user has admin custom claim
  allow read, write: if request.auth != null
    && request.auth.token.role == 'admin';
}

match /moderator-content/{docId} {
  // Allow access to admins and moderators
  allow read, write: if request.auth != null
    && request.auth.token.role in ['admin', 'moderator'];
}

Data Validation

match /posts/{postId} {
  allow create: if request.auth != null
    // Required fields exist
    && request.resource.data.keys().hasAll(['title', 'content', 'authorId'])
    // Title length validation
    && request.resource.data.title is string
    && request.resource.data.title.size() >= 1
    && request.resource.data.title.size() <= 100
    // Content validation
    && request.resource.data.content is string
    && request.resource.data.content.size() <= 10000
    // Author must be the current user
    && request.resource.data.authorId == request.auth.uid
    // Timestamp must be server time
    && request.resource.data.createdAt == request.time;

  allow update: if request.auth != null
    && resource.data.authorId == request.auth.uid
    // Prevent changing certain fields
    && request.resource.data.authorId == resource.data.authorId
    && request.resource.data.createdAt == resource.data.createdAt;
}

Firestore Rule Functions

Create reusable functions for cleaner rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Helper function: Check if user is authenticated
    function isAuthenticated() {
      return request.auth != null;
    }

    // Helper function: Check if user owns the resource
    function isOwner(userId) {
      return isAuthenticated() && request.auth.uid == userId;
    }

    // Helper function: Check if user has a specific role
    function hasRole(role) {
      return isAuthenticated() && request.auth.token.role == role;
    }

    // Helper function: Check if user is admin
    function isAdmin() {
      return hasRole('admin');
    }

    // Helper function: Validate string length
    function isValidString(field, minLen, maxLen) {
      return field is string
        && field.size() >= minLen
        && field.size() <= maxLen;
    }

    // Helper function: Get user document
    function getUserData() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
    }

    // Use the functions
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow write: if isOwner(userId);
    }

    match /admin/{document=**} {
      allow read, write: if isAdmin();
    }

    match /posts/{postId} {
      allow read: if true;
      allow create: if isAuthenticated()
        && isValidString(request.resource.data.title, 1, 100);
    }
  }
}

Subcollection Rules

Secure nested collections:

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

  // Subcollection: user's private notes
  match /notes/{noteId} {
    // Inherit parent's rules - only owner can access
    allow read, write: if request.auth.uid == userId;
  }

  // Subcollection: user's public posts
  match /publicPosts/{postId} {
    // Anyone can read, only owner can write
    allow read: if true;
    allow write: if request.auth.uid == userId;
  }
}

// Wildcard to match any subcollection depth
match /users/{userId}/{document=**} {
  allow read, write: if request.auth.uid == userId;
}

Cloud Storage Security Rules

Secure file uploads and downloads:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // User profile images
    match /users/{userId}/avatar.{ext} {
      // Anyone can read profile images
      allow read: if true;

      // Only owner can upload, with restrictions
      allow write: if request.auth != null
        && request.auth.uid == userId
        // Only allow image files
        && request.resource.contentType.matches('image/.*')
        // Max 5MB file size
        && request.resource.size < 5 * 1024 * 1024;
    }

    // User's private files
    match /users/{userId}/private/{allPaths=**} {
      allow read, write: if request.auth != null
        && request.auth.uid == userId;
    }

    // Public uploads (e.g., blog images)
    match /public/{allPaths=**} {
      // Anyone can read
      allow read: if true;

      // Only authenticated users can upload
      allow write: if request.auth != null
        && request.resource.contentType.matches('image/.*')
        && request.resource.size < 10 * 1024 * 1024;
    }

    // Restrict file types
    match /documents/{userId}/{fileName} {
      allow read: if request.auth.uid == userId;
      allow write: if request.auth.uid == userId
        // Allow only PDF and common document formats
        && request.resource.contentType in [
          'application/pdf',
          'application/msword',
          'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        ];
    }
  }
}

Realtime Database Security Rules

JSON-based rules for Realtime Database:

{
  "rules": {
    // Default: deny all access
    ".read": false,
    ".write": false,

    "users": {
      "$userId": {
        // Users can read their own data
        ".read": "$userId === auth.uid",

        // Users can write their own data with validation
        ".write": "$userId === auth.uid",

        // Validate user data structure
        ".validate": "newData.hasChildren(['name', 'email'])",

        "name": {
          ".validate": "newData.isString() && newData.val().length <= 50"
        },

        "email": {
          ".validate": "newData.isString() && newData.val().matches(/^[^@]+@[^@]+$/)"
        }
      }
    },

    "posts": {
      // Anyone can read posts
      ".read": true,

      "$postId": {
        // Only authenticated users can create posts
        ".write": "auth != null && (!data.exists() || data.child('authorId').val() === auth.uid)",

        ".validate": "newData.hasChildren(['title', 'content', 'authorId'])",

        "authorId": {
          ".validate": "newData.val() === auth.uid"
        }
      }
    },

    "presence": {
      "$userId": {
        // Users can only update their own presence
        ".write": "$userId === auth.uid"
      }
    }
  }
}

Testing Security Rules

Test your rules before deploying:

# Use the Firebase Emulator
firebase emulators:start

# Run rules tests
npm test

# Test file: tests/firestore.rules.test.js
const { initializeTestEnvironment, assertFails, assertSucceeds } = require('@firebase/rules-unit-testing');

let testEnv;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'demo-project',
    firestore: {
      rules: fs.readFileSync('firestore.rules', 'utf8')
    }
  });
});

afterAll(async () => {
  await testEnv.cleanup();
});

test('users can read their own data', async () => {
  const userId = 'user123';
  const context = testEnv.authenticatedContext(userId);
  const db = context.firestore();

  await assertSucceeds(db.collection('users').doc(userId).get());
});

test('users cannot read other users data', async () => {
  const context = testEnv.authenticatedContext('user123');
  const db = context.firestore();

  await assertFails(db.collection('users').doc('other-user').get());
});

test('unauthenticated users cannot write', async () => {
  const context = testEnv.unauthenticatedContext();
  const db = context.firestore();

  await assertFails(db.collection('users').doc('test').set({ name: 'Test' }));
});

Deploying Rules

Deploy your security rules:

# Deploy Firestore rules
firebase deploy --only firestore:rules

# Deploy Storage rules
firebase deploy --only storage

# Deploy Realtime Database rules
firebase deploy --only database

# Deploy all rules
firebase deploy --only firestore:rules,storage,database

# View deployed rules in Firebase Console
# Firestore: Firebase Console > Firestore > Rules
# Storage: Firebase Console > Storage > Rules
# Realtime DB: Firebase Console > Realtime Database > Rules

Common Security Mistakes

Avoid these common pitfalls:

Never do this:

// DANGEROUS: Allows anyone to read/write everything
match /{document=**} {
  allow read, write: if true;
}

// DANGEROUS: Only checks authentication, not authorization
match /users/{userId} {
  allow read, write: if request.auth != null;
}

Do this instead:

// SECURE: Check both authentication AND authorization
match /users/{userId} {
  allow read, write: if request.auth != null
    && request.auth.uid == userId;
}

// SECURE: Validate all incoming data
match /posts/{postId} {
  allow create: if request.auth != null
    && request.resource.data.authorId == request.auth.uid
    && request.resource.data.title is string
    && request.resource.data.title.size() > 0;
}

💡 Key Takeaways

  • • Security Rules are your first line of defense against unauthorized access
  • • Always validate both authentication AND authorization
  • • Use functions to keep rules DRY and readable
  • • Validate data structure and content in write rules
  • • Test rules thoroughly before deploying to production
  • • Never use allow read, write: if true in production

📚 Learn More

Continue Learning