You're viewing the legacy docs. They are deprecated as of May 18, 2016.
Go to current docs

Security & Rules Guide

Securing Your Data

Firebase provides a flexible, expression-based rules language with JavaScript-like syntax to easily define how your data should be structured and when your data can be read from and written to. The rules live on the Firebase servers and are automatically enforced at all times.

Overview

Firebase utilizes a powerful set of declarative Security and Firebase Rules, written in JSON, to control access and validation.

Declarative means that the rules are defined separate from the product logic. This has a number of advantages: clients aren't responsible for enforcing security, buggy implementations will not compromise your data, and perhaps most importantly, there is no need for an intermediate referee, such as a server, to protect data from the world.

Types of Rules

Security and Firebase rules come in three flavors: .write, .read, and .validate. Here is a quick summary of their purpose:

Rule Type Description
.read Describes if and when data is allowed to be read by users.
.write Describes if and when data is allowed to be written.
.validate Defines what a correctly formatted value will look like, whether it has child attributes, and the data type.

The rules are stored as JSON data similar to the following:

{
  "rules": {
    "foo": {
      // /foo/ is readable by the world
      ".read": true,

      // /foo/ is writable by the world
      ".write": true,

      // data written to /foo/ must be a string less than 100 characters
      ".validate": "newData.isString() && newData.val().length < 100"
    }
  }
}

If you don't specify rules for a certain node (as we did with the top-level node above), they will default to false.

Predefined Variables

There are a number of helpful, predefined variables that can be accessed inside a security rule definition. We'll be using most of them in the examples below. Here is a brief summary of each and a link to the appropriate API reference.

Variable Description
now The current time in milliseconds since Linux epoch. This works particularly well for validating timestamps created with the SDK's Firebase.ServerValue.TIMESTAMP.
root A RuleDataSnapshot representing the root path in the Firebase database as it exists before the attempted operation.
newData A RuleDataSnapshot representing the data as it would exist after the attempted operation. It includes the new data being written and existing data.
data A RuleDataSnapshot representing the data as it existed before the attempted operation.
$ variables A wildcard path used to represent ids and dynamic child keys.
auth Represents an authenticated user's token payload.

Existing Data vs. New Data

The predefined data variable is used to refer to the data before a write operation takes place. Conversely, the newData variable contains the new data that will exist if the write operation is successful. newData represents the merged result of the new data being written and existing data.

To illustrate, consider a rule that would allow us to create new records or delete existing ones, as long as data does not already exist at a given path, but not to make changes to the data:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

Referencing Data in other Paths

Any data can be used as criterium for rules. Using the predefined variables root, data, and newData, we can access any path as it would exist before or after a write event.

Consider this example, which allows write operations as long as the value of the /allow_writes/ node is true, the parent node does not have a readOnly flag set, and there is a child named foo in the newly written data:

{
  ".write": "root.child('allow_writes').val() === true &&
            !data.parent().child('readOnly').exists() &&
            newData.child('foo').exists()"
}

Rules Cascade

Security and Firebase Rules work from the top-down

This is a critical concept of understanding Security and Firebase Rules. The child rules can only grant additional privileges to what parent nodes have already declared. They cannot revoke a read or write privilege.

With the exception of .validate definitions, Security and Firebase Rules work from a top-down model. If a parent node grants read or write permissions, then it also grants access to all child nodes under it. Consider the following structure:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

This security structure allows /bar/ to be read from whenever /foo/ contains a child baz with value true. The ".read": false rule under /bar/ has no effect here, since read is already false by default, and access cannot be revoked by a child path.

While it may not seem immediately intuitive, this is a powerful part of the rules language and allows for very complex access privileges to be implemented with minimal effort. This will be illustrated when we get into user-based Security and Firebase Rules later in this guide.

Rules Are Not Filters

Rules are applied in an atomic manner. That means that a read or write operation is failed immediately if there isn't a rule at that location or at a parent location that grants access. Even if every child path is accessible, reading at the parent location will fail completely. Consider this structure:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

Without understanding that rules are evaluated atomically, it might seem like fetching the /records/ path would return rec1 but not rec2. The actual result, however, is an error:

ref.child("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Firebase *ref = [[Firebase alloc] initWithUrl:URL];
[[ref childByAppendingPath:@"records"] observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    // success block is not called
} withCancelBlock:^(NSError *error) {
    // cancel block triggered with PERMISSION_DENIED
}];
var ref = Firebase(url:URL)
ref.childByAppendingPath("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
ref.child("records").addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Since the read operation at /records/ is atomic, and there's no read rule that grants access to all of the data under /records/, this will throw a PERMISSION_DENIED error. If we evaluate this rule in the security simulator in our App Dashboard, we can see that the read operation was denied:

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

The operation was denied because no read rule allowed access to the /records/ path, but note that the rule for rec1 was never evaluated because it wasn't in the path we requested. To fetch rec1, we would need to access it directly:

ref.child("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Firebase *ref = [[Firebase alloc] initWithUrl:URL];
[[ref childByAppendingPath:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    // SUCCESS!
}];
var ref = Firebase(url:URL)
ref.childByAppendingPath("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
ref.child("records/rec1").addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Using $ Variables

Accessing dynamic paths in the rules can be done using a $ prefix. This serves as a wild card, and stores the value of that key for use inside the rules declarations:

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

The dynamic $ variables can also be used in parallel with constant path names. In this case, they specify any key that isn't explicitly listed should not be allowed to be written to the /widgets/ node:

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}
Path keys are always strings

Firebase path keys are always strings. For this reason, it's important to keep in mind that when we attempt to compare a $ variable to a number, this will always fail. This can be corrected by converting the number to a string (e.g. $key === newData.val()+'')

Validating Data

Enforcing data structures and validating the format and content of data should be done using .validate rules, which are run after a .write rule succeeds. Below is a sample .validate rule definition which only allows dates in the format YYYY-MM-DD between the years 1900-2099, which is checked using a regular expression.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"
Try it on JSFiddle

Click here to see this in action. Try writing different values to the input field.

The .validate rules are the only type of security rule which does not cascade. If any validation rule fails on any child record, the entire write operation will be rejected. Additionally, the validate definitions are ignored when data is deleted (that is, when the value is null).

The .validate rules are only evaluated for non-null values and do not cascade.

These might seem like trivial points, but are in fact significant features for writing powerful Security and Firebase Rules. Consider the following rules:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

With this variant in mind, look at the results for the following write operations:

var ref = new Firebase(URL + "/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref childByAppendingPath:@"size"] setValue: @99];
var ref = Firebase(url:URL)

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.childByAppendingPath("size").setValue(99);
Firebase ref = new Firebase(URL + "/widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{size: 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{size: "foo", color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{size: 21, color: "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Now let's look at the same structure, but using .write rules instead of .validate:

ANTIPATTERN: This is not a recommended practice
{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

In this variant, any of the following operations would succeed:

var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];</pre>
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

This illustrates the differences between .write and .validate rules. As demonstrated, all of these rules should be written using .validate, with the possible exception of the newData.hasChildren() rule, which would depend on whether deletions should be allowed.

Anonymous Chat Example

Let's put the rules together and create a secure, anonymous chat application. Here we'll list the rules, and a functional version of the chat is included below:

{
  "rules": {
    // default rules are false if not specified
    // setting these to true would make ALL CHILD PATHS readable/writable
    // ".read": false,
    // ".write": false,

    "room_names": {
      // the room names can be enumerated and read
      // they cannot be modified since no write rule
      // explicitly allows this
      ".read": true,

      "$room_id": {
        // this is just for documenting the structure of rooms, since
        // they are read-only and no write rule allows this to be set
        ".validate": "newData.isString()"
      }
    },

    "messages": {
      "$room_id": {
        // the list of messages in a room can be enumerated and each
        // message could also be read individually, the list of messages
        // for a room cannot be written to in bulk
        ".read": true,

        // room we want to write a message to must be valid
        ".validate": "root.child('room_names/'+$room_id).exists()",

        "$message_id": {
          // a new message can be created if it does not exist, but it
          // cannot be modified or deleted
          ".write": "!data.exists() && newData.exists()",
          // the room attribute must be a valid key in room_names/ (the room must exist)
          // the object to write must have a name, message, and timestamp
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",

          // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin"
          "name": { ".validate": "newData.isString() && newData.val().length > 0 \
              && newData.val().length < 20 && !newData.val().contains('admin')" },

          // the message must be longer than 0 chars and less than 50
          "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" },

          // messages cannot be added in the past or the future
          // clients should use Firebase.ServerValue.TIMESTAMP to ensure
          // accurate timestamps
          "timestamp": { ".validate": "newData.val() <= now" },

          // no other fields can be included in a message
          "$other": { ".validate": false }
        }
      }
    }
  }
}
Try it on JSFiddle

Feel free to try out some of the Security and Firebase Rules above by clicking on this interactive demo , which implements all the rules above in a functional chat.

Writing data with transactions

For a transaction to write data, it must also be able to read the path specified. So both read and write must evaluate to true before it can succeed.