loading...
Cover image for Why Firestore Encourages Bad Security

Why Firestore Encourages Bad Security

jbis9051 profile image Josh Brown Updated on ・8 min read

What is Firestore?

Firestore is one of many products in the Firebase product line. Firestore is a document-based NoSQL database. Firebase Auth integrates with Firestore to provide authentication/authorization functionality.

Why Firebase?

The benefit of using Firestore is that developers don't have to deal with the hassle of managing servers, creating a backend, or scalability. All of that is handled by Firebase. As such, Firestore is often used by frontend developers.

What this article is not about

Before I begin to explain why I believe Firestore encourages bad security, I'd like to clarify what I am not saying.

I am not saying it is impossible to make Firestore secure. It is very possible. However, as I will go onto explain, to use Firestore securely, you eliminate most of the benefits of using Firestore.

I am also not saying that Firestore itself is insecure. This article is about the security of the applications that implement Firestore. And how fundamentally Firestore encourages insecure implementations of Firestore.

Ok. Get to the gist. What's the issue?

Yet, more background

In order to understand the problem with Firestore, we must first understand why it's so appealing.

Client-server systems, especially websites, can almost always be broken down into three main layers (more info):

  1. Presentation layer - This is the frontend/user interface. The presentation layer receives data from the logical layer for displaying and sends input data to the logical layer for processing.

  2. Logical layer - The logical layer queries data from the data layer and passes it to the presentation layer to be displayed. The logical layer also receives input from the presentation layer and processes it before possibly sending it to the data layer.

  3. Data layer - The data layer stores data passed by the logical layer. The data layer also retrieves data for the logical layer based on given queries.

Firestore's appeal (and its flaw) is that it eliminates the logical layer entirely. Although removing layer #2 makes building applications speedier and easier, it forces developers to place all logic on the presentation layer. The presentation layer then accesses the data layer directly.

As Firestore docs put it,

Cloud Firestore supports SDKs for Android, IOS, and Web. Combined with Cloud Firestore security rules and Firebase Auth, the mobile and web SDKs support serverless app architectures where clients connect directly to your Cloud Firestore database. With a serverless architecture, you do not need to maintain an intermediary server between your clients and your Cloud Firestore database.

The Flaw

Layer #2, the logical layer, is where security around input occurs. This includes authentication, authorization (access control), validation, sanitization, rate limits, and much more. It also contains logical controls that shouldn't be manipulated by the user. For example, a button click gives a user 5 points. The user shouldn't be able to manipulate how many points they receive, the "increment user's points by 5" logic should be implemented server-side on the logical layer.

In a classical environment where the logical layer exists, the above example would look similar to this:

              Presentation Layer                                         Logical Layer                                               Data Layer
On button press, HTTP Request '/button-press' -->  If user x hasn't pressed the button, increment user points by 5 -->  Increment points of user with id x by 5

However using Firestore, it would look more like this

Presentation Layer (on the client)

button.addEventListener('click', () => 
    firestore.doc(`users/${x}`).update({
     points: firebase.firestore.FieldValue.increment(5)
    })
);

---> 

Data Layer (handled by Firestore)

Increment points of user with id x by 5

Since the logic is on the presentation layer which is on the client, the user can simply manipulate the applications logic and change it to something like

[...]
     points: firebase.firestore.FieldValue.increment(5000)
[...]

Layer #2 is critical to provide any kind of secure system. Without a middle man between the presentation layer and the data layer, the user can wreak all kinds of havoc. For more information, see this question on StackExchange Why can't I just let customers connect directly to my database?,

Firestore Security Rules

Firestore also supports security rules. Security rules, attempt to emulate parts of the logical layer on the data layer. Security rules allow developers to add extremely basic authentication, authorization (access control), and validation to their Firestore database. However, most of the time these security rules are inadequate. Developers can't import proper validation libraries so they must implement all validation themselves.

There are plenty of scenarios where security rules inflexibility can cause problems. One example, is a situation in which multiple queries need to executed on a given action. When a form is submitted the form data is added to one document and the user document is updated to change the has_filled_out_form to true. In Firebase it would look something like this,

db.collection("form").doc("<id>").set(formData); // Query #1
firestore.doc(`users/${x}`).update({has_filled_out_form: true}); // Query #2

To prevent the same user from submitting the form multiple times, a security rule could be added that says, "reject if has_filled_out_form is true". However, a user could easily bypass this by only executing query #1 multiple times, and never executing query #2. has_filled_out_form would remain false validating all the query #1s.

The fact is, Firestore security rules is an inadequate band-aid fix to a fundamental issue with Firestore. It attempts to replicate the role of the logical layer but falls short in almost every situation.

Ok, but what about Firebase Cloud Functions?

The only possible way to implement validation, authentication, authorization, and logic properly with Firestore, is by using Cloud Functions.

However, Cloud Functions are just a form of the logical layer. Instead of the client (presentation layer) accessing Firestore (data layer) directly, the cloud function (logical layer) acts as a middle man. It performs all the validation, authentication, authorization, and logic needed and then accesses Firestore (the data layer).

In order to use Firestore securely, you eliminate the main benefit (and differentiator) of using Firebase. At that point, you might as well use a much more performant database like MongoDB.

Regardless, Firestore doesn't expect you to use Cloud Functions. In fact, Firestore's docs encourage you to do the opposite. They provide numerous client side libraries and SDKs and provide tons of documentation on those libraries. In fact, the first example in their docs, uses the client side web SDK.

Most developers, aren't trained in security and don't consider how attackers could take advantage of security being solely implemented on the client. Developers focus primarily on functionality and design.

In May, security researchers found thousands of apps that allowed write access to their Firestore databases by anyone. This is a wide scale problem.

This is not just a problem in theory. In May, security researchers found thousands of apps that allowed write access to their Firestore databases by anyone. This is a wide scale problem.

Conclusion

Firestore is fundamentally flawed. By eliminating the logical layer, Firestore makes it nearly impossible to use it securely. Firestore security rules are an inadequate band-aid solution and cloud functions defeat the purpose of using Firestore.

There are very few situations where Firestore will be an adequate solution. I'd only recommend using it only extremely small and simple applications or mock applications, but even so, you may run into security limitations.


I'd love to hear your thoughts on this article both in agreement and disagreement. If you think I missed something, comment below and I'll take a look. Thanks!


Update 9/14/2020

As @Gaute Meek Olsen pointed out, the examples I gave in this article can be solved using Firestore security rules. See his comment for examples.

My response includes some additional points worth mentioning:

Thank you for your reply! You seem to be correct that those rules would work for the examples mentioned in this article.

However, I'd like to mention a couple of points to consider.

As your application starts to grow, if you implement all the necessary security rules (validation, authentication, authorization, logical checks, "batch checking", etc.), your firestore.rules will turn into a huge and messy and unmaintainable god file, making this solution not ideal for larger applications. Also, note that there are many limits on security rules. While those limits are pretty generous, it's worth considering when building larger applications, especially when considering the next couple of points. (Admittedly, this point is not directly related to security but it is still worth considering)

Additionally, as mentioned in the article, firestore security rules do not permit you to import libraries. This means you must either copy and paste source code for libraries you want to use or build a solution yourself. The "copy and paste" method exacerbates the "god file" and limits mentioned above. You must also keep up with updates to the library. The other option, not using a library, can be very dangerous. Most developers won't be able to implement let alone maintain their own implementation of security-related libraries (hence the need for the library in the first place).


I can easily create a server and a database and allow each request without writing any code for authentication, authorization, validation, etc. So the data can be both insecure and secure by using Firestore just the same as anything else.

Yes, you are correct. Anything can be implemented securely and insecurely. I'd like to explain why it's much easier to fail to implement firebase securely than a proper backend.

Most security issues relate to a set of wrong assumptions. Not checking for authentication is an assumption that the user is who they said they are. Not checking for authorization is an assumption that the user is allowed to do what they are trying to do. Poor validation is an assumption that the data the user submits is...well..valid.

All three of these specific assumptions aren't guaranteed in both firestore security rules and a classic server environment. This is we must implement checks to confirm that the user is in fact authenticated and authorized and the input data is valid.

However, there is a very important differentiator. Code written on the backend is guaranteed to run to completion without modification. This cannot be assumed to be true on the frontend, creating yet another layer of checks that needs to be implemented in security rules.

With the batch example from before, if implemented on a proper backend one can assume that both queries WILL run in the order you specified so there is no need to check that the second query will run.

The additional security rules you posted are needed because that same assumption cannot be made about code on the frontend. As you have proven with the fairly simple example of batch queries, it is possible add security rules to check this case but doing so adds another layer where developers who often are not great at security anyway, may make wrong assumptions and implement rules that don't fully check every possible situation.

Posted on by:

jbis9051 profile

Josh Brown

@jbis9051

I'm Josh Brown. I am 17 years old. I would like to go into cyber security, specifically application security.

Discussion

markdown guide
 

Well the flaws you mention are easily fixed with rules.

Only increment with 5 points

match /users/{id}{
  allow update: if request.resource.data.points - resource.data.points == 5 || !("points" in request.resource.data);
}

The or operator is for when other properties than points are updated.

Prevent user from submitting the form multiple times

use batch in frontend:

const batch = db.batch();
batch.set(db.collection("form").doc("<id>"), formData);
batch.update(db.collection("users").doc("<userid>"), {has_filled_out_form: true});
batch.commit()
match /form/{id} {
  allow write: if hasNotFilledForm() && hasFilledFormAfter();
}
function hasNotFilledForm(){
  return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.has_filled_out_form == false;
}
function hasFilledFormAfter(){
  return getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data.has_filled_out_form == true;
}

Conclusion

So I believe it boils down to knowing how to secure your data. I can easily create a server and a database and allow each request without writing any code for authentication, authorization, validation, etc. So the data can be both insecure and secure by using Firestore just the same as anything else.

 

You've nailed it on the head here.

The article fails to represent the power of security rules for validating user input and requests.

Your comparison to setting up a basic database logic layer is brilliant.

 
 

Thank you for your reply! You seem to be correct that those rules would work for the examples mentioned in this article.

However, I'd like to mention a couple of points to consider.

As your application starts to grow, if you implement all the necessary security rules (validation, authentication, authorization, logical checks, "batch checking", etc.), your firestore.rules will turn into a huge and messy and unmaintainable god file, making this solution not ideal for larger applications. Also, note that there are many limits on security rules. While those limits are pretty generous, it's worth considering when building larger applications, especially when considering the next couple of points. (Admittedly, this point is not directly related to security but it is still worth considering)

Additionally, as mentioned in the article, firestore security rules do not permit you to import libraries. This means you must either copy and paste source code for libraries you want to use or build a solution yourself. The "copy and paste" method exacerbates the "god file" and limits mentioned above. You must also keep up with updates to the library. The other option, not using a library, can be very dangerous. Most developers won't be able to implement let alone maintain their own implementation of security-related libraries (hence the need for the library in the first place).


I can easily create a server and a database and allow each request without writing any code for authentication, authorization, validation, etc. So the data can be both insecure and secure by using Firestore just the same as anything else.

Yes, you are correct. Anything can be implemented securely and insecurely. I'd like to explain why it's much easier to fail to implement firebase securely than a proper backend.

Most security issues relate to a set of wrong assumptions. Not checking for authentication is an assumption that the user is who they said they are. Not checking for authorization is an assumption that the user is allowed to do what they are trying to do. Poor validation is an assumption that the data the user submits is...well..valid.

All three of these specific assumptions aren't guaranteed in both firestore security rules and a classic server environment. This is we must implement checks to confirm that the user is in fact authenticated and authorized and the input data is valid.

However, there is a very important differentiator. Code written on the backend is guaranteed to run to completion without modification. This cannot be assumed to be true on the frontend, creating yet another layer of checks that needs to be implemented in security rules.

With the batch example from before, if implemented on a proper backend one can assume that both queries WILL run in the order you specified so there is no need to check that the second query will run.

The additional security rules you posted are needed because that same assumption cannot be made about code on the frontend. As you have proven with the fairly simple example of batch queries, it is possible add security rules to check this case but doing so adds another layer where developers who often are not great at security anyway, may make wrong assumptions and implement rules that don't fully check every possible situation.

 

This was a good reply with some good points I missed in the article 😊👏

 

I also think, the pricing of the firestore is pretty fair, the pricing of cloud function is also pretty good, but as soon as you have to use multiple of these services, it adds up.

Thanks for sharing your thoughts and experiences, I think they will be useful in some discussions.

 

other posters already mentioned that Rules fix the points mentioned. additionally you can use cloud functions for the cases where rules dont cut it. by no means you have to pick an all-or-nothing situation, which breaks your argument about making firestore pointless once you use cloud functions.
finally, there are still other advantages like reactive programming, which MongoDB doesnt give so its not a good replacement

 

You're right, by removing the so called backend and putting all the logic in the presentation layer (aka frontend) you are removing a very important layer for security. Don't get my wrong I use Firebase and its services for basically all my private projects, but if you plan to go big you have to thing about this issues. A while ago I tried to put all the Firebase logic in a seperate backend service, but well, that's not what Firebase is for in the first place.

I think you have to find a compromise when you choose Firebase for your Application. Maybe you invest more time in Firebase Functions, but as you said, it's also a bit flawed.

Anyways, I believe we'll see more and more websites and apps which merely relies on such frontend sdks like Firebase in the future. Why? Because you only need to learn JavaScript/TypeScript and you are ready to build a whole application with authentication, data storage, user management and many more.

 

Don't get my wrong I use Firebase and its services for basically all my private projects, but if you plan to go big you have to thing about this issues. A while ago I tried to put all the Firebase logic in a seperate backend service, but well, that's not what Firebase is for in the first place.

Yes. I understand this. Firebase is pretty easy and simple, so for a private project where security is not a huge concern it maybe a great solution.

Because you only need to learn JavaScript/TypeScript and you are ready to build a whole application with authentication, data storage, user management and many more.

With the introduction of node, with only TypeScript/JavaScript, you can also achieve a proper backend server with all the things you mentioned. I hope that this will convince more users to create a proper backend, but I agree, due to firebase's ease it seems to be becoming more popular.

 

Thanks for the write up, I'm considering building a large app on top of firebase, everyone's comments are great and valid.

 

Awesome!

I encourage you to take a look at my comment in response to @Gaute Meek Olsenabove above. I think the points are worth considering especially for a larger application.

 

My other fear is if I needed to move away from Firestore for any reason. Is that even possible as far as ease of mIgration to say MongoDB or FaunaDB..