DEV Community

Cover image for Firebase Security Rules
Chandra Panta Chhetri
Chandra Panta Chhetri

Posted on • Edited on

Firebase Security Rules

What is Firebase?

Firebase is a platform that provides easy integration of commonly used services for mobile and web applications. These services include

  • Firestore
  • Cloud Functions
  • Authentication
  • Cloud Storage
  • Realtime Database
  • Hosting
  • Firebase ML

How do security rules come into play?

Traditionally, access to a database is controlled via server-side code. However, Firebase services such as Firestore allow client-side code to access the database directly. This poses a security risk as client-side code can be viewed by anyone. This is where Firebase's concept of security rules come into play. Firebase security rules control access to specific resources. In other words, we can avoid the hassle of writing, maintaining, and deploying server-side code that controls access to resources. Security rules can only be written for Realtime Database, Firestore, and Cloud Storage.

Writing Security Rules

Security rules act as a middleware for when a service is used (e.g. reading a document in Firestore). When a request to access a service is denied due to security rules, the entire request fails. Rules can be written via Firebase console or an IDE that is using the Firebase CLI. Writing rules in the console provides the benefit of using Rules Playground before deploying the rules.

Firestore Security Rules

Firestore Security Rules consist of match statements and allow expressions. Match statements identify documents in your database and allow expressions control access to those documents.

The basic structure of a security rule:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /<some_path>/ {
      allow read: if <some_condition>;
      allow write: if <some_condition>;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we are matching some path in the database and allowing read and write permissions based on a condition. In situations where we need finer access control, we can split read and write into more granular operations.

read can be broken into

  • get (applies to single document read requests)
  • list (applies to queries and collection read requests)

write can be broken into

  • create
  • update
  • delete

We can also define custom functions to improve readability and reusability.

Let's take a look at an example

rules_version = '2';  
service cloud.firestore {
  match /databases/{database}/documents {  
    function isAdmin() {
      let userDoc = get(/databases/$(database)/documents/users/$(request.auth.uid));
      return userDoc == null ? false : userDoc.data.isAdmin;
    }
    function isAuthenticated(){
        return request.auth != null;
    }
    match /users/{userId}{
        allow read, delete: if isAuthenticated() 
                            && request.auth.uid == resource.id;
        allow create: if request.resource.data.keys().hasAll(['name', 'email', 'isAdmin']) 
                      && request.resource.data.keys().hasOnly(['name', 'email', 'isAdmin'])
                      && request.resource.data.isAdmin == false;
        allow update: if isAuthenticated()
                      && request.auth.uid == resource.id
                      && request.resource.data.isAdmin == resource.data.isAdmin;
    }
 }
}
Enter fullscreen mode Exit fullscreen mode

Based on the example above, we have written the following rules for the users collection:

  • reading user(s) and deleting a user is allowed if the user is authenticated & the user document being read/deleted belongs to the authenticated user
  • creating a user is allowed if the request body is of the form {name, email, isAdmin} && the isAdmin field is false (to prevent creating admin accounts from client-side)
  • updating a user is allowed if the user is authenticated, the user document being updated belongs to the authenticated user, and the isAdmin field remains unchanged

You might be wondering where variables and methods such as get(), request, resource are coming from. Well, any custom functions or match statements in the service cloud.firestore namespace has access to the following:

Properties

  • request (request context)
    • Contains auth and incoming request information via auth & resource property
    • request.resource is used to enforce write operations (create, update, delete) have specific structure and field validations (e.g. age >= 15)
    • request.auth is used to provide access based on if the request is authenticated
    • More info can be found at https://firebase.google.com/docs/reference/rules/rules.firestore.Request
  • resource
    • resource being written to or read (i.e. document in a collection)
    • Document data and id can be accessed via resource.data & resource.id
    • Useful for enforcing certain fields cannot be changed. In the example above, we are preventing the isAdmin field from changing in update user requests by request.resource.data.isAdmin == resource.data.isAdmin.
    • More info can be found at https://firebase.google.com/docs/reference/rules/rules.firestore.Resource

Methods

  • get(path) - path must be absolute path to a document
    • Gets the contents of a document
    • Useful when the modification of a document depends on another document
    • e.g. The isAdmin() function in the example above gets the user document based on request.auth.uid then accesses the isAdmin field to check if the user is an Admin

The complete list of methods and properties provided in the service cloud.firestore namespace can be found at https://firebase.google.com/docs/reference/rules/rules.firestore

Firebase also provides functions for working with different property types (e.g. list, boolean, map...etc) when writing rules. In the example above, we are using hasOnly() and hasAll() to enforce a user document only has 'name', 'email', and 'isAdmin' field.

Cloud Storage Security Rules

Security rules for Cloud Storage are similar to Firestore rules except instead of matching documents in a collection, we match directories or paths to files.

Let's take a look at an example

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /product_images/{imageName}
    {
        allow read, delete: if request.auth != null;
        allow create: if request.auth != null 
                      && request.resource.contentType.matches('image/.*')
                      && request.resource.size < 100000;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the similarities between Firestore rules and Cloud Storage rules. The main difference being service firebase.storage namespace provides us with different properties on the request variable.

Based on the example above, we have written the following rules for the product_images directory:

  • reading and deleting files is allowed if the user is authenticated
  • files can be added if the user is authenticated & the file is an image file that is smaller than 100 000 bytes

Realtime Database Security Rules

Firebase's Realtime Database stores data as a large JSON object. This means when writing security rules, we match keys in the object instead of documents in a collection (like in Firestore).

The basic structure of a security rule:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Before writing rules, an important concept to keep in mind is that read & write rules cascade. In other words, if a rule grants read & write access at a particular path, then it grants access to all child nodes. Furthermore, shallower rules override deeper rules.

Let's take a look at an example

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "true",
        "bar": {
          /* bar can be read so this rule is ignored since read 
             was allowed already */
          ".read": false
        }
     }
  }
}
Enter fullscreen mode Exit fullscreen mode

Firebase Realtime Database provides pre-defined variables similar to how service firebase.storage namespace provides global variables & methods.

A more practical example:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid",
        ".validate": "newData.hasChildren(['name', 'age'])",
        "age": {
            ".validate": "newData.isNumber() &&
                          newData.val() > 0"
         }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Based on the example above, we have written the following rules for the users node:

  • write & read access is allowed user node belongs to the authenticated user
  • a child node in the users node must have a 'name' & 'age' property
  • the 'age' property must be a number greater than 0

Conclusion

Although different services (Firestore, Cloud Storage, Realtime Database) have slightly different syntax when writing rules, at a high level we are doing the same thing. More specifically, we match a path to a resource and control access to that resource via read and write conditions.

Nevertheless, Firebase security rules serve the purpose of protecting data from malicious users. Thus, I hope this article helped you understand the main concepts when writing security rules! Now go protect your data!

Top comments (0)