I store data in a Cloud Firestore database. Users in my app donĀ“t need to create an account to get data and they can also write data without to login.
Google reminds me every few days that my database is insecure and can be abused by anyone. How can I improve it without accessing Auth variables?
My firebase rules
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Is there a way to make my database more secure without using authentication?
The logic of my app:
My database contains surnames and their origin. If someone enters a name, he gets the origin back from the database. Example: "Doe" -> "Mexican". If the last name does not exist in my database, I call an API and save the value to my database. Every user needs both read and write permission.
What can I do here?
Since the operation that you require writes for is limited (only inserting new items) you have some options:
You could deny writes to end user clients, and instead send a request to a cloud function that does exactly the operation you need (after verifying the input, or any other checks you might want, rate limiting, etc). Cloud functions ignore the security rules as they run with administrative access.
Here is a sample node function that performs a write to a realtime database, and it succeeds when both read and write are false in the security rules (your associated package.json obviously needs to depend on firebase-admin and firebase-functions):
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
let db = admin.firestore();
// This pushes the "text" parameter into the RDB path /messages/(hash)/original
exports.addMessage = functions.https.onRequest(async (req, res) => {
// Grab the text parameter.
const original = req.query.text;
// Push the new message into the Realtime Database using the Firebase Admin SDK.
const snapshot = await admin.database().ref('/messages').push({original: original});
// Respond to the user (could also be a redirect).
res.send('got it: ' + snapshot.ref.toString());
});
You may want to read about how the firebase admin SDK does access control but within a cloud function you should have admin rights by default.
Using the rules language you could only allow create operations. This removes the ability of the client to update or delete existing data. This isn't quite as secure as the prior method, but might be ok for you:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow create;
}
}
}
Also, note this works for firestore (which you are using) but not for realtime database.
Obviously both of these methods could be in some way abused to write lots of data into your database, though the former gives you a lot more control about what is allowed (e.g. you could prevent more than N entries, or more than Y bytes per entry). The later still lets anyone create whatever they want.
The first thing is to start with the documentation. It's strongly recommended that you have an understanding of what rules can do, and translate that into requirements for your app.
What you're describing for your app right now is too vague to come up with good rules. To be honest, without Firebase Authentication, it's not possible to accept writes to a database without Authentication and also avoid abuse, since anyone could write anything from anywhere on the internet. This could also cost you large amounts of money if someone discovers your "open" database.
Check out this documentation. https://firebase.google.com/docs/firestore/security/rules-structure. Configure the writing of unauthenticated users only in the collections you specify.
service cloud.firestore {
match /databases/{database}/documents {
// authentication required
function issignedin() {
return request.auth != null;
}
// authentication not required
function notAuthenticated() {
return request.auth == null;
}
// A read rule can be divided into get and list rules
match /cities/{city} {
// Applies to single document read requests
allow get: if notAuthenticated();
// Applies to queries and collection read requests
allow list: if notAuthenticated();
}
// A write rule can be divided into create, update, and delete rules
match /cities/{city} {
// Applies to writes to nonexistent documents
allow create: if notAuthenticated();
// Applies to writes to existing documents
allow update: if notAuthenticated();
// Applies to delete operations
allow delete: if notAuthenticated();
}
}
}
as a consideration, this will be insecure if the calling API allows indiscriminate writing.
Note: If the API you are referring to is the only one you can write, you must configure only the reading as public
service cloud.firestore {
match /databases/{database}/documents {
// authentication required
function issignedin() {
return request.auth != null;
}
// authentication not required
function notAuthenticated() {
return request.auth == null;
}
// A read rule can be divided into get and list rules
match /cities/{city} {
// Applies to single document read requests
allow get: if notAuthenticated();
// Applies to queries and collection read requests
allow list: if notAuthenticated();
}
// A write rule can be divided into create, update, and delete rules
match /cities/{city} {
// Applies to writes to nonexistent documents
allow create: if issignedin();
// Applies to writes to existing documents
allow update: if issignedin();
// Applies to delete operations
allow delete: if issignedin();
}
}
}
Related
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.
I'm working on a Flutter app using Firebase as a backed. I've set up group based roles in Firebase and the rules simulator in Firebase tells me the user I'm testing has access to the document. When I do a query in my Flutter code, I can see it finds the document and I can see it for a split second before it changes it mind and I get a "Listen for query at students failed: Missing or insufficient permissions." and the document is removed from the snapshot.
The query I use in the Flutter code is as follows:
Firestore.instance.collection('students').where('test', arrayContains: userID).orderBy('name').snapshots()
I have been playing with the document and tried different approaches for the current user to query for the document, and just to test it out I created an array with the userId and look for that.
If I completely skip the rules and just put the "need to be logged in" as requirement then I get a document back but as soon as I use the role based one then it's back to the drawing board. The rules I've set up are:
service cloud.firestore {
match /databases/{database}/documents {
match /students/{student} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
allow read, write : if isOneOfRoles(resource,['teacher', 'student', 'parent']);
}
}
}
Any idea what's causing this?
I have a basic rule on my Firestore Cloud database. I want to give user cafes that user is in but i am always getting the error "Missing or insufficient permissions.". What is the thing that I miss?
service cloud.firestore {
match /databases/{db}/documents {
match /cafes{
allow read: if request.auth.uid != null;
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
}
}
}
here is the code that i use to reach to documents
export class MyApp {
private rootPage:any;
// rootPage:any = HomePage;
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, afAuth: AngularFireAuth, afs: AngularFirestore) {
platform.ready().then(() => {
statusBar.styleDefault();
splashScreen.hide();
afAuth.auth.onIdTokenChanged(user=>{
if(user){
console.log(user.uid)
afs.collection('cafes').valueChanges().subscribe(console.log)
} else {
console.log('logged out')
}
})
});
}
}
One of the key sentences from the docs on security rules is
Every database request from a Cloud Firestore mobile/web client
library is evaluated against your security rules before reading or
writing any data.
and this should answer your question: You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document.
If you read this rule
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
as "Allow every user to view a cafe document if it contains his user id in the participants list" it becomes clear that Firestore needs to read the document to answer the question.
What you can try is allow read on all cafe documents and then query for those "the user is in" with the implication that users can read the other cafes as well.
You should take a look at this official guide for a concept on role based access on Firestore documents.
Update
I want to be more precise on the statement "You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document."
You cannot query for all documents and expect to get those visible to the requesting user (like you do in afs.collection('cafes'), i. e. security rules are not filters.
What you can do is qualify (add a where clause) your query to only include the documents visible to the user. Firestore will match the qualified query against your security rules and if both match, fetch the desired results.
In Firebase Firestore, I'm trying to allow only (custom-assigned) admins to write/update/delete resources, and for that I've got these security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /resources {
allow read;
allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
}
match /resources/{resource} {
allow read;
allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
}
}
}
I'm signing in with the user that is marked as an admin in the users collection:
NfwIQAjfNdS85yDvd5yPVDyMTUj2 is the UID gotten from the Authentication pane:
However, for some reason (UPDATE: reasons identified; see answer), I'm getting PERMISSION_DENIED errors when writing to the resources collection after being absolutely sure I'm signed in with the admin user.
Perhaps it is possible to view request logs from Firestore? Then I could have a look at what request.auth.uid looks like to match it up with my collections and rules.
While writing my question, I made it work! I made two mistakes, both of which could have been avoided if I read the docs properly.
Firstly, all calls to the service-defined function get needs to prefix the path with /databases/$(database)/documents/. So that this rule:
allow write: if get(/users/$(request.auth.uid)).isAdmin;
becomes this:
allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin;
It's long, I know, but that's how it is. I'm not sure why Firestore isn't able to do that by itself, though, seeing as that same path prefix will stay the same across all calls to get, but perhaps this is for some future feature that isn't ready yet, like cross-database querying or something.
Second, the get function will return a resource, which in turn you'll need to call .data on to get the actual data that it contains. Thus, instead of doing this:
get(/path/to/user/).isAdmin
you'll need to do this:
get(/path/to/user/).data.isAdmin
Now I just wish I was able to extract that logic into a user-defined function:
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
}
But doing so results in a PERMISSION_DENIED again, and without knowing what's actually going on in the function, I'm not sure if I'll spend more time trying to figure this out now.
UPDATE: #Hareesh pointed out that functions must be defined within the scope of a matcher, so it's possible to put the function in the default top-level matcher like this:
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
// ...
}
}
Some points i noticed
match /resources is pointing to a collection, that rules has no effect on its documents. here i am quoting from the doc
Rules for collections don't apply to documents within that collection. It's unusual (and probably an error) to have a security rule that is written at the collection level instead of the document level.
so you don't have to write rules for collections
Then in the rules allow write, update, delete: you can say either allow write: or specifically allow create, update, delete: any of the three options or combine them.
try this
service cloud.firestore {
match /databases/{database}/documents {
match /resources/{resource} {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
}
allow read;
allow create, update, delete: if isAdmin();
}
}
}
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.