Realtime Database Rules are declarative configuration for your database. This means that the rules are defined separately 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.
Structuring Your Rules
Realtime Database Rules are made up of JavaScript-like expressions contained in a JSON document. The structure of your rules should follow the structure of the data you have stored in your database. For example, let's say you are keeping track of a list of messages and have data that looks like this:
{ "messages": { "message0": { "content": "Hello", "timestamp": 1405704370369 }, "message1": { "content": "Goodbye", "timestamp": 1405704395231 }, ... } }
Your rules should be structured in a similar manner. Here's an example set of rules that might make sense for this data structure.
{ "rules": { "messages": { "$message": { // only messages from the last ten minutes can be read ".read": "data.child('timestamp').val() > (now - 600000)", // new messages must have a string content and a number timestamp ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()" } } } }
Types of Security Rules
There are three type of rules for enforcing security: .write
, .read
,
and .validate
. Here is a quick summary of their purposes:
Rule Types | |
---|---|
.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. |
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.
Predefined Variables | |
---|---|
now | The current time in milliseconds since Linux epoch. This works particularly well for validating timestamps created with the SDK's firebase.database.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. |
These variables can be used anywhere in your rules. For example, the security
rules below ensure that data written to the /foo/
node must be a string
less than 100 characters:
{ "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" } } }
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, this rule would allow us to create new records or delete existing ones, but not to make changes to existing non-null 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 criterion 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()"
Read and Write Rules Cascade
.read
and .write
rules work from top-down, with shallower
rules overriding deeper rules. If a rule grants read or write permissions at a particular
path, 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 /foo/bar/
has no
effect here, since 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 later in this guide.
Note that .validate
rules do not cascade. All validate rules
must be satisfied at all levels of the hierarchy in order for a write to be allowed.
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 affected 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:
JavaScript
var db = firebase.database(); db.ref("records").once("value", function(snap) { // success method is not called }, function(err) { // error callback triggered with PERMISSION_DENIED });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // success block is not called } withCancelBlock:^(NSError * _Nonnull error) { // cancel block triggered with PERMISSION_DENIED }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in // success block is not called }, withCancelBlock: { error in // cancel block triggered with PERMISSION_DENIED })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records"); ref.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 }); });
REST
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 Firebase console, 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:
JavaScript
var db = firebase.database(); db.ref("records/rec1").once("value", function(snap) { // SUCCESS! }, function(err) { // error callback is not called });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // SUCCESS! }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in // SUCCESS! })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records/rec1"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { // SUCCESS! } @Override public void onCancelled(FirebaseError firebaseError) { // error callback is not called } });
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1 # SUCCESS!
Query-based Rules
Although you can't use rules as filters, you can limit access to subsets of data
by using query parameters in your rules. Use query.
expressions in your
rules to grant read or write access based on query parameters.
For example, the following query-based rule uses user-based security rules
and query-based rules to restrict access to data in the baskets
collection
to only the shopping baskets the active user owns:
"baskets": {
".read": "auth.uid != null &&
query.orderByChild == 'owner' &&
query.equalTo == auth.uid" // restrict basket access to owner of basket
}
The following query, which includes the query parameters in the rule, would succeed:
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
However, queries that do not include the parameters in the rule would fail with
a PermissionDenied
error:
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
You can also use query-based rules to limit how much data a client downloads through read operations.
For example, the following rule limits read access to only the first 1000 results of a query, as ordered by priority:
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
// Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
The following query.
expressions are available in Realtime Database Rules.
Query-based rule expressions | ||
---|---|---|
Expression | Type | Description |
query.orderByKey query.orderByPriority query.orderByValue |
boolean | True for queries ordered by key, priority, or value. False otherwise. |
query.orderByChild | string null |
Use a string to represent the relative path to a child node. For example,
query.orderByChild == "address/zip" . If the query isn't
ordered by a child node, this value is null.
|
query.startAt query.endAt query.equalTo |
string number boolean null |
Retrieves the bounds of the executing query, or returns null if there is no bound set. |
query.limitToFirst query.limitToLast |
number null |
Retrieves the limit on the executing query, or returns null if there is no limit set. |
Validating Data
Enforcing data structures and validating the format and content of data should be done using
.validate
rules, which are run only after a .write
rule succeeds to grant access. 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])$/)"
The .validate
rules are the only type of security rule which do 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 new value
being written is null
).
These might seem like trivial points, but are in fact significant features for writing powerful Firebase Realtime Database 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:
JavaScript
var ref = db.ref("/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);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"]; // 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 child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget") // 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.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("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);
REST
# 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
:
{ "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:
JavaScript
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);
Objective-C
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];
Swift
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)
Java
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);
REST
# 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.
Using $ Variables to Capture Path Segments
You can capture portions of the path for a read or write by declaring
capture variables with the $
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 example, we're using the $other
variable to declare
a .validate
rule that ensures that
widget
has no children other than title
and color
.
Any write that would result in additional children being created would fail.
{ "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 } } } }
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.database.ServerValue.TIMESTAMP // to ensure accurate timestamps "timestamp": { ".validate": "newData.val() <= now" }, // no other fields can be included in a message "$other": { ".validate": false } } } } } }
Next Steps
- Learn how to secure user data using security rules.
- Learn more about specifying indexes using rules.