Nobody likes to write security rules. Its language is limited. They hard to write, test, and maintain, and the price of mistakes is high. Every time you have to change them, it always feels like a lot of work. So no surprise that they often get neglected.
However, it's not something that you can afford to ignore. All you can do is divide the work into approachable steps, starting with the most straightforward rules that you can implement in minutes and then work on nitty-gritty details that will make your security bulletproof.
In this article, I list such steps that will help you organize your work on the rules and make your app truly secure.
It seems obvious, but that’s widespread mistake to hope that
request.auth.uid != null would be enough.
Thankfully it’s a low hanging fruit that will protect your users' data, which should always come first.
Start with this rule to get to the 80% of result within just a couple minutes:
allow read, write: if resource.data.userId == request.user.uid
It’s intuitive to keep all user-related information in a single document so you can retrieve or update it with a single request, but that exposes you to security issues.
Do you want users to be able to change their subscription plan without paying for it, set arbitrary email, or use 3rd-party service tokens that you store along with their profile data? I don't think so so!
Fix it by creating a separate collection (i.e.,
usersSystemData) not available to read and store there all
sensitive data users don't need access to. For convenience, use user id to
store that data.
The same applies to other collections that might include sensitive information.
When you protected your data from unauthorized reads, it's time to secure the writes! This step requires the most effort and further maintenance but will ensure the integrity of your application.
An innocent update of a field with a wrong value might disrupt the work of the whole application. For example, if you have a function that loops all users, an operation on data of an incorrect type will throw an exception and stop execution for the rest of the users. Or if when you calculate statistics on all data, the single inconsistent value might ruin the result.
You can never trust user input, so validate all fields on create and update to ensure the consistency of your data.
Unfortunately, not all data can be reliably validated, and not everything can be done within security rules. If you work on a high-risk application, work with money, have to make a network request to validate the data, etc. you can use Firestore function triggers to validate further data created by the user.
To do so, create allow writing documents only with a special field that would indicate that the validation is pending and update it when thefunction trigger has processed the document.
Even though reads are incredibly cheap, if you have collections with a huge number of documents, a single query request might considerably increase your bill. To prevent that or make data scrubbing more challenging, you can limit the number of documents that can be returned in a single query.
request.query.limit to check if requested limit is within
allow list: if request.query.limit <= 25
Follow those steps, and your application security will be truly bulletproof!