Firebase Realtime Database Security Rules allow you to control access to data stored in your database. The flexible rules syntax allows you to create rules that match anything, from all writes to your database to operations on individual nodes.
Realtime Database Security 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.
This topic describes the basic syntax and structure Realtime Database Security Rules used to create complete rulesets.
Structuring Your Security Rules
Realtime Database Security 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.
Basic rules identify a set of nodes to be secured, the access methods (e.g., read,
write) involved, and conditions under which access is either allowed or denied.
In the following examples, our conditions will be simple true
and
false
statements, but in the next topic we'll cover more dynamic ways to
express conditions.
So, for example, if we are trying to secure a child_node
under a parent_node
,
the general syntax to follow is:
{ "rules": { "parent_node": { "child_node": { ".read": <condition>, ".write": <condition>, ".validate": <condition>, } } } }
Let's apply this pattern. 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 a set of rules for read-only security that might make sense for this data structure. This example illustrates how we specify database nodes to which rules apply and the conditions for evaluating rules at those nodes.
{ "rules": { // For requests to access the 'messages' node... "messages": { // ...and the individual wildcarded 'message' nodes beneath // (we'll cover wildcarding variables more a bit later).... "$message": { // For each message, allow a read operation if <condition>. In this // case, we specify our condition as "true", so read access is always granted. ".read": "true", // For read-only behavior, we specify that for write operations, our // condition is false. ".write": "false" } } } }
Basic Rules Operations
There are three types of rules for enforcing security based on the type of
operation being performed on the data: .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. |
Wildcard Capture Variables
All rules statements point to nodes. A statement can point to a specific
node or use $
wildcard capture variables to point to sets of nodes at a
level of the hierarchy. Use these capture variables to store the value of node
keys for use inside subsequent rules statements. This technique lets you write
more complex Rules conditions, something we'll cover in more detail
in the next topic.
{ "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 } } } }
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 because no read rule allowed access to the
/records/
path. However, 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!
Overlapping Statements
It's possible for a more than one rule to apply to a node. In the
case where multiple rules expressions identify a node, the access method is
denied if any of the conditions is false
:
{ "rules": { "messages": { // A rule expression that applies to all nodes in the 'messages' node "$message": { ".read": "true", ".write": "true" }, // A second rule expression applying specifically to the 'message1` node "message1": { ".read": "false", ".write": "false" } } } }
In the example above, reads to the message1
node will be
denied because the second rules is always false
, even though the first
rule is always true
.
Next steps
You can deepen your understanding of Firebase Realtime Database Security Rules:
Learn the next major concept of the Rules language, dynamic conditions, which let your Rules check user authorization, compare existing and incoming data, validate incoming data, check the structure of queries coming from the client, and more.
Review typical security use cases and the Firebase Security Rules definitions that address them.