Firebase 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
Firebase 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. |
.write
or .read
rule is specified at or above a path, access will be denied.
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()"Make sure to check for null or invalid data. Errors in rules lead to rejected operations.
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:
!data.parent().child('readOnly').exists() &&
newData.child('foo').exists()"
Read and Write Rules Cascade
Note: Shallower security rules override rules at deeper paths. Child rules can only grant additional privileges to what parent nodes have already declared. They cannot revoke a read or write privilege..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:
[[ _ref child :@ "records" ] observeSingleEventOfType : FIRDataEventTypeValue withBlock :^( FIRDataSnapshot * snapshot ) {
// success block is not called
} withCancelBlock :^( NSError * _Nonnull error ) {
// cancel block triggered with PERMISSION_DENIED
}];
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:
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" ] observeSingleEventOfType : FEventTypeValue withBlock :^( FIRDataSnapshot * snapshot ) {
// 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 Firebase 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.
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 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
).
.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 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:
// 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 ];
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:
// 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 ];
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 }
}
}
} Note: 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()+''
)
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 }
}
}
}
}
}Try it on JSFiddle: Feel free to try out some of the Firebase Realtime Database 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.
Next Steps
- Learn how to secure user data using security rules.
- Learn more about specifying indexes using rules.