The Firestore vulnerability found in Arc is likely widespread

The Firestore vulnerability found in Arc is likely widespread

 
xyz3va disclosed an Arc vulnerability recently, caused by incorrectly configured Firestore security rules. If you’re using Firestore at your company, you should read this post - it’s reasonably likely that your setup is vulnerable.

Proliferation

So, how common is this vulnerability in other Firestore applications?
Well, almost every resource I could find on how to use Firestore rules, including the official Firestore docs, recommend an implementation that’s vulnerable to this attack.
You should expect that many Firestore applications online right now would allow you to create an arbitrary doc on the behalf of another user.
Perhaps developers have generally proactively caught this? Perhaps most schemas are designed so that it’s fine if you can create a document for another user? But I doubt it.
A summary of search results:
  • Fireship.io
    • The example in “Secure by Owner, Has-Many Relationship” is secure, but the other example is vulnerable.
  • Reddit (link 1)
    • The only answer is vulnerable
  • Medium (link 1, link 2)
    • Both guides recommend vulnerable implementations.

Attack

The underlying Firestore attack goes as follows:
  • The attacker creates an account user1, and then creates a document doc1 belonging to user1
  • The attacker gifts doc1, changing the owner from user1 to user2
    • The correct set of Firestore security rules ought to prevent this step, but in the case of Arc and other vulnerable applications, this is not prevented.
  • When user user2 fetches associated documents for their account, they now have an additional document doc1
The outcome is that the attacker can create arbitrary documents for any user, if they know their user id. They cannot read, modify, or delete existing documents.
The severity of this attack is application specific. For Arc, the existence of a boosts document results in the execution of a custom JS script, allowing an attacker to run arbitrary JS.

Vulnerable logic

The vulnerable logic lives in an application’s Cloud Firestore Security Rules, ie: Firebase’s DSL for specifying access control. Here’s a snippet based on the Firestore docs.
rules_version = '2'; service cloud.firestore { // Applies to all databases in this account match /databases/{database}/documents { // Applies to all documents in all collections match /{collection}/{document} { allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.owner_uid allow create: if request.auth != null && request.auth.uid == request.resource.data.owner_uid } } }
resource.data refers to the current state of the document
request.resource.data refers to the state of the document if the request goes through.
This ACL logic above hence does the following:
  • Only allows a user to read, update, or delete a document if they own it
  • Only allows a user to create a new document that belongs to them.
However, as xyz3va discovered, that does not prevent a user from changing the ownership of a document to another user.
And here’s the fixed code:
rules_version = '2'; service cloud.firestore { // Applies to all databases in this account match /databases/{database}/documents { // Applies to all documents in all collections match /{collection}/{document} { allow read, delete: if request.auth != null && request.auth.uid == resource.data.owner_uid allow update: if request.auth != null && request.auth.uid == resource.data.owner_uid && request.auth.uid == request.resource.data.owner_uid allow create: if request.auth != null && request.auth.uid == request.resource.data.owner_uid } } }
 
Here’s a firestore.test.rules test that you can add to your suite to see if you have the vulnerability in your codebase:
test('change owner of doc denied', async () => { await testEnvironment.withSecurityRulesDisabled(async (context) => { await context .firestore() .doc('arbitrary/doc') .set({ owner_uid: 'user1' }) }) expect(() => // Attempting to change the owner of a doc away from oneself should fail! user1.doc('arbitrary/doc').update({ owner_uid: 'user2' }), ).rejects.toThrow() })