Firebase Security & Rules

Grab a drink and settle in for a long, cozy ride

Chris Esplin
How To Firebase
10 min readApr 7, 2017

--

Firebase is a cloud datastore that’s accessible directly from any connected client. Because any client can connect to any Firebase, you must write security rules to secure your data. Failure to write proper security rules will leave you wide open to attack. But don’t worry! You’re about to learn everything you need to lock your data up tight.

Keep your security rules simple. Security rules can become overly complex and get out of hand quickly if your data structure is not well thought out. This module demonstrates some “best” practices. You’ll likely need to modify them a bit for your production apps… just think through the implications carefully if you choose to deviate from these patterns. And make sure you’ve read this entire document and all of the Security & Rules docs before you get fancy.

Security rules are node-based

Security rules are managed by a single JSON object that you can edit on your Realtime Database console or using the Firebase CLI. And as a handy bonus!, the console and the CLI will warn you if your rules are malformed.

The rules object starts out like this:

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

The root node of the JSON object must be named rules. The rules node defaults to read/write false. The above example explicitly sets .read and .write to false, but leaving the rules blank would accomplish the same thing.

Rule Cascading

Firebase rules cascade in such a way that granting a read or write privilege to a parent node always grants that read/write access to all child nodes.

Read that again.

Firebase rules cascade in such a way that granting a read or write privilege to a parent node always grants that read/write access to all child nodes.

This is a huge “gotcha”, even for experienced Firebase users. Let’s review why Firebase handles it this way, and how we can work around this limitation.

First, the very nature of Firebase demands this kind of behavior. If you query a node in Firebase, you get all of the child nodes. Firebase is so focused on performance that it could never take the time to evaluate rules on all child nodes and potentially delete them from the parent node. It would be an enormous performance hit.

Second, you can easily avoid problems with rule cascading by structuring your data according to your permissions structure. Nest your data under high-level nodes named things like “admin”, “userReadable”, “userWriteable” or “userOwned”. Grouping objects with similar security needs under high-level nodes will dramatically reduce the number of security rules that you need to maintain.

Note On Validation Rule Hacks: We’ll cover validation rules later in this module. Validation rules can be used to block writes on children of a parent node for which a user has write privileges. So if you grant write privileges to userOwned/preferences/{uid}, you can write a validation rule for, say userOwned/preferences/{uid}/isAdmin that would prevent the user from updating the isAdmin child node.

There are consequences to this approach, primarily that any ref.set() action on userOwned/preferences/{uid} will fail, because you can’t overwrite the isAdmin child node… you’d need to use ref.update() to update each child node individually, which can be obnoxious. It would be easier to move your isAdmin node to a different part of your data structure that’s already user readable but not writeable. You could call it userReadable/preferences/{uid}/isAdmin.

Example Data Structure

Let’s use the following data structure for the rest of this section:

That’s a doozy. Take some time to soak it all in and then let’s discuss.

Ok. This data starts with four top-level nodes:

  1. users
  2. userReadable
  3. userWriteable
  4. userOwned

We also have two users:

  1. Kanye West
  2. Taylor Swift

The users node contains basic user data, in this case just email addresses. We’re imagining that when Kanye authenticated with our app his user was assigned the uid kanyesUID. Likewise, Taylor received the uid taylorsUID. These UIDs will form the basis of our security model.

First, let’s make each user object user-readable…

{
"rules": {
"users": {
"$uid": {
".read": "auth.uid == $uid"
}
}
}
}

We’ve added a users node to the root node, rules. Any rules placed directly under the users node will cascade to all child nodes… but we don’t want to set rules at the users node! We want to set rules for each individual user based on the user’s uid, so we create a wildcard node, $uid. We can name wildcard nodes whatever we like, as long as the first character is $. Best practice is to name them something descriptive, so we’ve called this wildcard node $uid, because each user is getting saved under his or her uid.

Wildcard nodes apply to all otherwise unspecified nodes. Confused? Check this out!

See what we did there? We grant only .read access under the wildcard $uid node, but we’ve granted both .read and .write access to Taylor’s user.

You’re likely wondering what those “auth.uid == $uid” lines mean. Whenever a user authenticates with Firebase Authentication, that user receives a uid from the system that is accessible via auth.uid. So when Kanye authenticates with the system, his auth.uid equals kanyesUID. Likewise, Taylor’s auth.uid is taylorsUID. We can compare auth.uid to the $uid wildcard value to grant users read access to their own nodes, but not to anyone else’s nodes. We can also compare directly to strings like so: “.read”: “auth.uid == ‘taylorsUID’”

Index child keys to speed up queries

Imagine that our app will run a query like

ref.child('users').orderByChild('email').equalTo('kwest@gmail.com').once('value', callback);

We’re asking firebase to search the users node for a child node with an email node equal to kwest@gmail.com. This could be a large, slow query depending on our database size, so Firebase offers us high-performance indexing on select child nodes. In this case, we want Firebase to index all users by their email child nodes. We do this with a quick, easy security rule, “.indexOn”: “email”. See below 👇

{
"rules": {
"users": {
".indexOn": ["email"],
"$uid": {
".read": "auth.uid == $uid"
}
}
}
}

If we had another child node to index, say users/{$user}/username, our rule would read “.indexOn”: [“email”, “username”]. Piece of cake.

Traverse the tree for detailed permissions

Let’s address the third user in our imaginary twitter-based app: Ryan Seacrest. Ryan’s been sitting on the sidelines thus far, but he’s critical to this process, because his is an admin account. Notice the “isAdmin”: true attribute on his user account? We can read that straight from the rules. Check this out.

Users can still read their own account data, but we just added read and write privileges to any user with the isAdmin flag on their account. The rule to detect this flag is root.child(‘users’).child(auth.uid).child(‘isAdmin’).val(). The root object represents the root node of our Firebase. We can then call .child(‘nodeName’) to traverse down the tree all the way to the isAdmin node of our auth’d user. See how we used auth.uid to dive in to the authed user? I agree… it’s clever. It will also fail silently for any user that’s not authenticated, so these nodes will be secure!

One more note: We have to call .val() on a node to get it’s value. That’s reminiscent of how we interact with data snapshots with the Javascript Firebase SDK, so it should look familiar.

Another note: There are other ways to traverse the tree. We could have called data.parent().child(auth.uid).val() to traverse from the node we’re securing (the users/{$uid} node) up to users and back down again. This method can be useful, but traversing from root is reliable, because root is the same from any part of our app. We often find ourselves copying and pasting security rules as we build out our data models, and you can’t copy/paste a rule beginning in data… without being extremely careful about what data represents… so try to stick to root for safety’s sake.

Use the Bolt rules compiler to streamline your rules

Thus far we’ve been manipulating our rules object directly in the JSON. This is fine, but it can get repetitive with the copy/paste. The Firebase team got sick of the copy/paste as well and wrote a little compiler that integrates with the firebase-tools CLI to make rules easy to write.

You’ll need to install Bolt with npm install -g firebase-bolt and create a file in your project root named something like security-rules.bolt. You can then run firebase-bolt security-rules.bolt, which will compile your Bolt rules into a new file and call it firebase-rules.json. The generated JSON is a bit more verbose than the JSON we’ve written thus far, but the Bolt syntax is nice and terse. See the Bolt language reference for all of the juicy details.

Notice how we wrote two functions at the top, isUser(auth, uid) and isAdmin(auth)? These functions are optional, but super useful. They let us reuse logic throughout our rules.

The next section has a bunch of path blocks. Use /nodeName]/ to wildcard part of your path. These wildcard values are available within that path’s scope, so you can access uid from /userReadable/objectType/uid and pass it into read() {isUser(auth, uid) || isAdmin(auth) }.

Structure your data according to read/write privileges

You may have noticed that three of our top-level nodes are named

  • userReadable,
  • userWriteable, and
  • userOwned.

In the last section we went ahead and granted user read privileges to children of userReadable, write privileges to children of userWriteable and full read/write privileges to children of userOwned.

This is an arbitrary data structure… we could have named these three nodes apples, pears and pineapples. But we’re not insane, so we named them something descriptive. We then used wildcard paths (see path /userReadable/objectType/uid) to apply rules to groups of object types.

Now we don’t have to write rules for every kind of object we create. We simply nest the objects under the node with the appropriate permissions and we’re good. For example…

The userOwned node has read/write privileges to all “object types” as long as the nested node matches the user’s uid. So only Kanye can access /userOwned/preferences/kanyesUID, and Kanye gets full read and write. Kanye could also create a new data node at /userOwned/grammyNominations/kanyesUID and write a nice list of his Grammy nominations to rub in Taylor’s face. We used two wildcards in the path /userOwned/objectType/uid, and we haven’t locked down the objectType wildcard in any way, so Kanye can go crazy with different “object types” as long as he nests his data under his own uid while he’s at it.

Remember that we can override wildcards by specifying another rule for a matching path. For instance…

path /userOwned/{objectType}/{uid} {
read() { isUser(auth, uid) || isAdmin(auth) }
write() { isUser(auth, uid) || isAdmin(auth) }
}
path /userOwned/grammyNominations/taylorsUID {
read() { true }
write() { uid == kanyesUID }
}

The second rule overrides the two wildcards in /userOwned/objectType/uid and gives only Kanye write privileges to Taylor’s Grammy Nominations list. It also makes Taylors list world-readable, so any website or user anywhere on the internet could hit https://<some firebase>.firebaseio.com/userOwned/grammyNominations/taylorsUID.json to get the Kanye-curated list of Taylor’s Grammys.

Validate your data while you secure it

read(), write() and index() cover most use cases, but sometimes you’ll find yourself wanting more detailed control of the types of data that can be written to a node.

Bolt lets you create data types that you can reuse across multiple nodes.

path /userOwned/preferences/{uid} is Preferences;
type Preferences {
validate() { this.excuse.length < 20 && this.respectRating < 5 }
useAutotune: Boolean,
excuse: String,
respectRating: Number
}

Notice that we can mix in a validation rule with validate()! The output from these validation rules is ugly. You can create validations in a security rules JSON file using “.validate”, but it’s much, much easier to just use Bolt.

You can also mix in types and read/write privileges like so:

path /userOwned/preferences/{uid} is Preferences {
read() { isUser(auth, uid) }
write() { isUser(auth, uid) }
}
type Preferences {
validate() { this.excuse.length < 20 && this.respectRating < 5 }
useAutotune: Boolean,
excuse: String,
respectRating: Number
}

Alias rules

So far we’ve used only read(), write(), index() and validate(), but Bolt supports three other “functions”:

  • create()
  • update()
  • delete()

These function are aliases for write(), so they cannot be mixed into the same path /.. block with a write() rule. They create write() rules that apply only to create, update or delete actions. The gist is that if you want to grant a user access to create objects but not edit or delete them, create() will generate the write() rule to handle that case.

path /dropbox/{objectType}/{uid} {
create() { isUser(auth, uid) }
read() { isAdmin(auth) }
}

path /editable/{objectType}/{uid} {
update() { isUser(auth, uid) }
read() { isAdmin(auth) }
}

path /deleteable/{objectType}/{uid} {
delete() { isUser(auth, uid) }
read() { isAdmin(auth) }
}

Deploy Your Rules

There are two ways to depoy your rules:

  1. Run firebase-bolt my-rules-file.bolt which will create my-rules-file.json which you can then copy/paste into your Realtime Database rules console.
  2. Run firebase-tools init to create your firebase.json hosting file, then create a node at database.rules that points to your .bolt or .json rules file. Then run firebase deploy — only rules to deploy your rules from the command line.

We’ll cover more firebase.json options later. For now, check out the example firebase.json example below and notice the database.rules node. It points to a file that I’ve called database.rules.bolt where I’ve written my bolt rules. If this file were titled database.rules.json, firebase would know that I had my rules in JSON format, but since I’ve named it .bolt, it will automatically pass them through the firebase-bolt compiler before sending them up to my Firebase in the cloud.

Example firebase.json

Exercise

Install bolt with npm install -g firebase-bolt and create a file named security-rules.bolt. Spec out the following rules in security-rules.bolt and run firebase-bolt security-rules.bolt to generate a file named security-rules.json.

  • /tweets/uid/tweetKey
  • use create() to enable users to create tweets but not edit or delete them
  • readable to the world
  • is of type Tweet, which has a string message and a numeric timestamp
  • type Tweet has a “message” attribute which is a string with fewer than 140 characters
  • type Tweet has a “timestamp” attribute which is numeric
  • /notifications/uid/notificationKey
  • readable by user
  • no write rule… will be server-managed
  • is of type Notification
  • type Notification has a string “user” attribute with fewer than 60 characters
  • type Notification has a boolean “read” attribute

Play around with these rules until you get them all to compile. Review the generated security-rules.json file to check your result.

Skim the Bolt language spec to get an idea of just how much control is available. Bolt and the security rules framework gives you detailed control of your data. You probably won’t use most of it, but be aware of what’s available in case you get stuck while developing your app and need more advanced security or validation.

--

--

Chris Esplin
How To Firebase

Front-End Web Developer; Google Developer Expert: Firebase; Calligraphy.org