Security Rules

To protect your Firebase data, you can define rules that describe how your data can be read and written. These rules live on the Firebase servers and are automatically enforced at all times. You can think of these rules as "trusted code" run by Firebase to enforce your security policy and data schema so that malicious users (or buggy application code) can't violate them.

To specify security rules for your Firebase, enter your Firebase URL in the browser and then click on the "Security" tab.

Rules Structure Overview

Rules are specified in a JSON object that matches the structure of your Firebase data tree, but with custom security rules attached at various locations. These rules restrict access and validate your data schema. For prototyping, your Firebase comes with two default rules that allow anyone to read and write anywhere in your Firebase:

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

Since the rules structure matches your data structure, you could change these rules to allow read access to everything, but write access only to /users/fred with:

{
  "rules": {
    ".read": true,
    "users": {
      "fred": {
        ".write": true,
      }
    }
  }
}

We'll see later that rule expressions are extremely flexible, but for now we're just using the expression 'true' which just grants access for all clients.

Types of Rules

There are three types of rules that you can use:

  • .read rules are used to grant clients permission to read data (e.g. via an on( ) call or the REST API).

  • .write rules are used to grant clients permission to write data (e.g. via a set( ) call or the REST API).

  • .validate rules are used once a .write rule has granted access, to further validate the data being written to make sure it's valid. In addition to a .write granting access, all relevant .validate rules must succeed before a write is allowed.

Note that a transaction( ) operation actually requires .read access in addition to .write access, since the client-side nature of Firebase transactions require the client to be able to read the data.

It's important to understand that a .read or .write rule grants access to a location, including all of its children, regardless of any other .read or .write rules that may be present. So in /users/fred example above, the .read rule at the top grants read access anywhere in the Firebase, and even if there was a ".read": false rule under /users/fred, clients could still read / users/fred because the .read rule at the top already granted access.

Rule Expressions

So far, all of our rules have universally allowed read or write access at a particular location by using "true" as the rule expression. In practice, however, you'll want dynamic rules that take into account the client's authentication details, the exact path being accessed, and perhaps other Firebase data. To do this, you can use a JavaScript-like expression as your rule instead of just "true".

Rule expressions are built on a set of predefined variables that are made available to you:

Variable Description
auth A client who has authenticated will have an auth payload stored in this variable.
$ variables When you have a "$ Location" in your rules structure, you can use a matching $ variable within your rule expression to get the name of the actual child being read or written.
now The current date/time as the number of milliseconds since the unix epoch according to the Firebase servers
root A RulesDataSnapshot corresponding to the current data at the root of your Firebase.
newData A RulesDataSnapshot for .write and .validate rules which provides data corresponding that will result after the write.
data A RulesDataSnapshot corresponding to the current data at in Firebase location.

$ Locations

As mentioned, the rules structure mirrors the structure of your Firebase data. This works great when you know the exact names of the children expected at a particular location. For instance, if we know that "/users" can have children called "fred" and "wilma" we can explicitly name them in our rules. But it's more likely that /users/ can have an arbitrary number of users with arbitrary names. For these cases, you can use special $ Locations in your rules, which will be used for any children not specified explicitly in your rules. For example we could have explicit rules for 'fred' and 'wilma' but also a set of rules for every other user in our Firebase by using a $ location:

{
  "rules": {
    "users": {
      "fred": {
        ".read": true,
        ".write": true
      },
      "wilma": {
        ".read": true
      },
      "$other": {
        "name": {
          ".read": true
        }
      }
    }
  }
}

In this example, clients can read and write /users/fred and they can read /users/wilma, but they can also now read the "name" child of any other user (e.g. /users/jack/name, /users/abc123/name, etc.) due to the $other rule that we added. As we'll see later, you can name "$other" anything you like as long as it begins with a $ (e.g. $user or $foo). $ variables can then be used as part of nested rule expressions.

Supposing the previous rule definitions were in effect for the SampleChat Firebase and there already exists children 'fred', 'wilma', and 'barney' under /users, here are some client operations you could try, with comments to indicate which ones would succeed or fail:

var url = 'https://<YOUR-FIREBASE>.firebaseio.com/users';
var usersRef = new Firebase(url);
usersRef.on('child_added', function(snapshot) { /* ... */ });
// WILL FAIL since there is no .read rule on / or /users to allow us to
// read here.

usersRef.child('fred').on('value', function(snapshot) { /* ... */ });
// WILL SUCCEED since there is a .read rule on /users/fred.

usersRef.child('fred/name/first').set('Freddy');
// WILL SUCCEED since there is a .write rule on /users/fred, so all writes
// at or below there will be allowed.

usersRef.child('fred').remove();
// WILL ALSO SUCCEED due to the same .write rule on /users/fred.

usersRef.child('wilma').remove();
// WILL FAIL since there is no .write rule on /users/wilma/ or above.

usersRef.child('barney/name/first').on('value', function(snapshot) {
/* ... */ });
// WILL SUCCEED due to the .read rule on /users/$other/name.

usersRef.child('barney/birthday').on('value', function(snapshot) {
/* ... */ });
// WILL FAIL since there's no rule to allow reading on
// /users/barney/birthday.

If the client trying to read or write is authenticated, the payload from their auth token is available within your rule expressions via a variable called auth. See Authentication for details on auth tokens. Suppose the user is authenticated and their auth token contains their username, you could use a rule like the following to give a special 'admin' user full read/write access to everything.

{
  "rules": {
    ".read": "auth.username == 'admin'",
    ".write": "auth.username == 'admin'"
  }
}

When you have a $ location in your rules (e.g. for '$foo'), you can use $foo within your rule expression to access the name of the actual child being read or written. So suppose we want to give every user read and write access to their own /users/ location. We could use:

{
  "rules": {
    "users": {
      "$user": {
        ".read": "$user == auth.username",
        ".write": "$user == auth.username"
      }
    }
  }
}

You can access existing firebase data by using the data variable, which gives you a RulesDataSnapshot for the data in your Firebase at the current rule location. So if we wanted to allow anybody to read from /users/ if it contains a child 'public' with the value true, we could use:

{
  "rules": {
    "users": {
      "$user": {
        ".read": "data.child('public').val() == true"
      }
    }
  }
}

Note you can also get a RulesDataSnapshot for the root of your Firebase using the root variable.

For .write rules, newData provides a RulesDataSnapshot for the new data that will result if the write is allowed. So if you wanted to let any client write to /total_messages, but only if they are writing a number, you could use:

{
  "rules": {
    "total_messages": {
      ".write": "newData.isNumber()"
    }
  }
}

Note: Read below on .validate rules for a better way to do this!

For full details on everything that's possible within rule expressions, see Security Rules Overview.

.Validate rules

A write operation will be allowed if any .write rule allows it. If there are multiple .write rules, only one of them needs to allow it. So consider the following modification of the previous example:

{
  "rules": {
    ".write": "auth != null",
    "total_messages": {
      ".write": "newData.isNumber()"
    }
  }
}

Only one of the two .write rules needs to be true for a write to /total_messages to succeed, so an authenticated user will be allowed to write any arbitrary data, even if it's not a number. This likely isn't what you wanted. This is where .validate rules come in.

For a write to be allowed, only one .write rule needs to evaluate to true, but all .validate rules for the new data being written must pass. This allows you to craft rules to make sure clients can only write valid data to your Firebase. So for instance, we could allow users to write to their own /users/ location with a .write rule, but force the data to conform to a particular schema using .validate rules:

{
  "rules": {
    "users": {
      "$user": {
        ".read": "$user == auth.username",
        ".write": "$user == auth.username",
        ".validate": "newData.hasChildren(['name'])",
        "name": {
          ".validate": "newData.hasChildren(['first', 'last'])",
          "first": {
            ".validate": "newData.isString()"
          },
          "last": {
            ".validate": "newData.isString()"
          }
        },
        "age": {
          ".validate": "newData.isNumber() && newData.val() >= 0"
        },
        "about_me": {
          ".validate": "newData.isString() && newData.val().length >= 10"
        }
      }
    }
  }
}

These validation rules ensure that every user has a 'name' child with 'first' and 'last' children which must both be strings. Additionally, a user can have an 'age' child which must be a non-negative number and an 'about_me' child which must be a string at least 10 characters long (see String for other string operations available).

.validate rules are only executed for the (non-null) data being written. So a .validate rule for a child being deleted does not need to pass for the write to be allowed. This means the following is insufficient to ensure that a 'name' always has a 'first' child:

{
  ...
  "name": {
    "first": {
      ".validate": "newData.isString()"
    }
  }
}

You instead need to put a rule on the 'name' location:

{
  ...
  "name": {
    ".validate": "data.hasChild('first')",
    "first": {
      ".validate": "newData.isString()"
    }
  }
}

To additionally prevent any extra children (e.g. to prevent a 'middle' child from being added under 'name'), you can add an $other rule that always fails. For example:

{
  ...
  "name": {
    ".validate": "data.hasChildren(['first', 'last'])",
    "first": { ... },
    "last": { ... },
    "$other": {
      ".validate": false
    }
  }
}

Supposing the previous rule definitions were in effect for the SampleChat Firebase, here are some client operations you could try, with comments to indicate which ones would succeed or fail:

var url = 'https://<YOUR-FIREBASE>.firebaseio.com/users/fred';
var fredRef = new Firebase(url);

// Assume we've authenticated to the SampleChat Firebase as
// user 'fred' and /users/fred is currently empty.
fredRef.set({ name: { first: 'Fred' } });
// WILL FAIL since 'name' must always have a 'last' child.

fredRef.set({ name: { first: 'Fred', last: 'Flintstone' } });
// WILL SUCCEED since Fred now has a first and last name.

fredRef.child('name/first').remove();
// WILL FAIL since 'name' must always have both a 'first' and 'last' child.

fredRef.child('age').set('seventeen');
// WILL FAIL since age must be a number.

fredRef.child('age').set(17);
// WILL SUCCEED since Fred now has a first/last name and an age.

fredRef.child('name').remove();
// WILL FAIL since a user must always have a 'name'.

fredRef.child('age').remove();
fredRef.child('name').remove();
// WILL SUCCEED since after removing 'age', only 'name' remains under
// /users/fred.

// So removing 'name' will effectively remove /users/fred entirely, and
// /users is not required to have a 'fred' child, so this will succeed.

Summary

This covers Firebase Security rules. To quickly recap the basics:

  • Security Rules are stored as a JSON structure that corresponds to your Firebase data structure.

  • You use .read, .write, and .validate to attach Security Rules Overview to locations in your Firebase.

  • Rule expressions use the client's authentication details as well as Firebase data to determine whether operations should be allowed or denied.


Have a suggestion to improve the documentation on this page? Tell us!