How to detect forward slashes in Firestore document IDs with security rules? - google-cloud-firestore

I have a usernames collection. Each of its documents has an id equal to the username of an app user, and a single field userId, which is a document ID in a parallel users collection. If a user changes his name to John52, I create a new document with an ID John52, delete the old username document, and update the username field in several other documents in other collections using a batched write.
I would like to prevent the creation of certain usernames, and I use the following security rule to achieve that:
function isSignedIn() {
return request.auth.uid != null
}
function existingData() {
return resource.data
}
match /usernames/{username} {
allow get: if isSignedIn();
allow create: if username.size() >= 5 && username.size() <= 12 &&
username.matches("[[:alpha:]]*") == true &&
username.lower().matches(".*duck.*") == false;
allow delete: if isSignedIn() && request.auth.uid == existingData().userId;
}
It is supposed to allow the creation of a new document only if its ID is: (1) between 5-12 characters in length, (2) alphanumeric, and (3) not a bad word.
The problem is that this rule does not prevent usernames with forward slashes at the beginning or the end of the otherwise valid text, e.g., usernames /John52 and John52/ are both allowed. The document ID that I can see in the Firebase console is still John52 (without slashes), so I suspect that perhaps Firebase somehow treats those slashes in a special way, or drops them, and the {username} in the security rule has no slashes anymore.
I know that document IDs should not contain forward slashes. However, if the ID gets set from the client, what is the best way to prevent it, and is it possible to do with the security rules?

However, if the ID gets set from the client, what is the best way to prevent it, and is it possible to do with the security rules?
First, the Firebase client SDK will remove the slashes from the document ID before that create/update document request is sent so value of username will never contain /.
If someone tries to use the Firestore REST API and bypass the SDK, they'll get an error says:
{
"error": {
"code": 400,
"message": "Resource id \"/test\" is invalid because it contains \"/\".",
"status": "INVALID_ARGUMENT"
}
}
The best you can do is replace / with any other character (e.g. =) but render it as a slash on frontend (i.e. .replace('=', '/')).
Alternatively, you can store all the username in an array but a document only has 1 MB max size limit and won't scale well. Routing write requests through a Cloud Function would be another option where you can encode the username into a base64 string and use it as document ID. You can then decode it while rendering.
The client SDK will remove only trailing or leading slashes.
const docRef = doc(db, 'books', '/test/')
console.log(docRef.id) // <-- 'test'
await setDoc(docRef, { ... })
If you have multiple forward slashes and create odd number of path segments, then it'll throw an error as the path would represent a collection and not a document.

Related

Flutter Firestore Security Rules

I've been trying to get the Firestore rules to play nice for a while now and every time I think I get them right, another portion stops working for some reason.
I'm trying to have some simple rules, if you made the document that document and any child documents or collections, you can create, edit and delete them. I thought this was pretty simple but alas I keep getting permission denied errors.
Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /users/{userId}/{document=**} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
When doing just the match /users/{userId} I was able at one time able to create user documents but I couldn't create child documents or colletions.
When doing just the match /users/{userId}/{document=**} I could no longer create users but any existing users I could add child documents and collections and do everything expected.
This combination of both rules doesn't seem to work either.
I keep getting [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation. when I try to create a user with this statement:
await FirebaseFirestore.instance.collection('users').doc(googleUser.uid).set(
{
'created': now,
'lastLogin': now,
'name': name,
'email': email,
},
);
Now nothing works. I deleted all my authentication accounts and my Firestore data and wanted to start over but it simply will not create the data in Firestore.
Any suggestions would be greatly appreciated as I'm going in circles and nothing is working anymore which is extremely frustrating as it did at one point but no longer does.
edit All of my testing is being done on a real Android phone.
After walking away from my computer and thinking more, I figured out what it was. My App Check debug token changed somehow.
Once I added the new value from the debug console everything started working again.
I'll leave this answer here in case this saves anyone else some headaches in the future!
Edit: Additionally, ones App Check debug token will change anytime you clear storage on your app on the device. Which is why mine was changing.

How do i write a Firebase Storage security rule that only allows write file permissions for auth users to a nested directory and none its parents?

I have the following security rule set for a Firebase Storage bucket:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /avatars/{userId}/{profileId}/{allPaths=**} {
allow write: if request.auth != null && request.auth.uid == userId
}
}
}
This works assuming the dynamic subdirectory userId matches the request.auth.uid.
However, this rule also allows that same user to write a file to the subdirectory {userId} and to write additional directories into profileId.
Id like to allow the authenticated user to generate both of the wildcard subdirectories as i have specified, but ONLY allow FILES to be uploaded into the last directory: profileId.
So...
attempting to upload a file into /avatars/${userId}/{profileId}/file.jpg should succeed.
attempting to write a file into /avatars/${userId}/file.jpg should fail.
attempting to create a directory in /avatars/${userId}/{profileId}/directoryName should fail.
attempting to upload a file into /avatars/${userId}/{profileId}/directoryName/file.jpg should fail.
Is this not possible?
Firstly, it's important to understand that Cloud Storage does not have "directories". There is no operation to create a "directory". There are only objects with paths that can have / separators to make it easier for you to organize content.
The reason why users can write to nested paths under profileId is because you are using a trailing recursive wildcard match {allPaths=**}, which allows writes under any possible path under profileId. I suggest reviewing the documentation on wildcards to better understand the behavior. It sounds like you don't want a recursive wildcard match at all, and instead just a single path segment match, e.g. {imageId}.

can't read path variable in firestore rules

I have some rules like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /teams/{teamId} {
allow read, write, delete, list:
if debug(teamId) in debug(request.auth.token.teams)
allow create:
if true;
}
}
}
I am using claims to control access. Each user has an array of teams in their token. That part is working fine.
Basically, teamId (coming from the path) doesn't exist. Nothing is printed in the log for this variable. I can't figure out whats going on. Is there some different way to access that teamId variable?
by doing some logs, it seems that for accessing /teams/teamXXXX I'm getting multiple rule hits. First on /database/default/documents/teams and then again on /database/default/documents/teams/teamXXX The first rule pass is failing because {teamId} is not defined on that path. I need to somehow allow access to the collection while limiting access to the child documents
It looks like the way I'm accessing teams may be causing a problem. I'm getting teams by doing a query like:
instance.collection("teams").where('owners',
arrayContains: FirebaseAuth.instance.currentUser!.uid);
This must be triggering the rule check where no {teamId} in the path. I thought about wrapping my match statement like:
match /teams/{document=**}
allow read:
if (request.auth.uid != "")
match /teams/{teamId} {
allow read, write, delete, list:
if debug(teamId) in debug(request.auth.token.teams)
allow create:
if true;
}
}
I'm worried that this will just allow all documents. I'm stuck.
answering my own question.
When doing a search / list on a collection, it still matches the rule of /teams/{teamId} even though you are not specifically querying by teamId.
Meaning a search like instance.collections("teams").where(...) will still match /teams/{teamId}
In the match, {teamId} will be blank. Instead, you must look at the input paramters coming in via the "resource" variable. The items you have in the "where" clauses will appear in the resource variable. You must use the resource data to resolve your rules.
So I had to separate the "list" rule form the rest of the rules.

Firestore Security Rules: How to get document and check if resource property is true

I need some help writing a Firestore security rule.
I have a subcollection called posts like the following example path:
/users/XrMD3azk4Jess5KNTSICv4RYEj02/posts/what-architecture-style-is-this_r3nosJHQIt
I am building a "mystery blog" app where users can submit mysteries (aka, "posts") and other users can provide answers (aka, "comments") to the mystery post. If the mystery post author feels that the provided comment solves the mystery, then the post author can go ahead and click a button labeled "Select as answer".
Example screenshot:
The answer is saved to a subcollection called answers in the user's record (the user that provided the answer, aka "doss"). This is an example path with screenshot:
This works fine.
However, I obviously only want the post author to be allowed to press the "select as answer" button...and I will write the front-end logic for that so that button only appears for the post owner. But, how can I correctly write the back-end firestore rule to prevent a non-post owner user from maliciously trying to edit/select an answer?
I assume that I need to check the post author's id (aka, uid) and see if it matches the signed in user's uid.
This is what I tried, but I get permission errors here:
match /users/{uid}/answers/{docId} {
allow read;
allow write: if request.auth != null; && get(/databases/$(database)/documents/users/$(request.auth.uid)/posts/$(id)).data.uid == uid
}
Here's an example of the post document that contains the author's uid and the post id:
Thanks for any help or pointers!
So I went a different path just to make this a bit easier on the security rule logic and maintenance.
I simply added a postUid property when creating the answer doc:
async selectAsAnswer(comment) {
const answerDoc = this.$fire.firestore
.collection(`users/${this.comment.uid}/answers`)
.doc()
await answerDoc.set({
id: answerDoc.id,
commentId: comment.id,
postId: comment.postId,
postUid: this.post.uid,
createdAt: this.$fireModule.firestore.FieldValue.serverTimestamp(),
commentUid: comment.uid
})
}
Then, in the security rule for this answer document I checked for the postUid property match:
match /users/{uid}/answers/{docId} {
allow read;
allow create, update: if signedInAndVerified() && request.resource.data.postUid == request.auth.uid;
}
This seems to work fine and it just makes the overall DX a bit easier.

Firestore Security Rules: Insufficient permissions

I need some help making my security rules for firestore work.
These are my firestore rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
function isAdmin(uid) {
return (uid in get(/admin/administrators).data.uid)
}
allow read;
allow write: if request.auth.uid != null && isAdmin(request.auth.uid);
}
}
}
In document /admin/administrators there is a field named uid containing an array of UIDs of administrators which are allowed to write in the database.
After I logged in as one such administrator and tried to add a document to another collection. (Specifically, the call in my Angular application using Angularfire 2 is this.afStore.collection(collection).add({});) I received the error Error: Missing or insufficient permissions.
Any help appreciated (including "there's obviously a better way to do this")
You should enter the absolute path of the collection you're trying to reference. Change the (uid in get(/admin/administrators).data.uid) to get(/databases/$(database)/documents/admin/$(request.auth.uid)).data.uid