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:
- Official Firestore Docs
- Basic Security Rules: vulnerable
- Avoid Insecure Rules: vulnerable (”Content Owner” and “Mixed Public and Private” examples)
- Update: I reported the issue to Google on Sept 23, and they notified me on Oct 4 that the above insecure examples were fixed. I verified on Oct 7 that the examples on the official docs are now fixed, and are no longer vulnerable.
- 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
Attack
The underlying Firestore attack goes as follows:
- The attacker creates an account
user1
, and then creates a documentdoc1
belonging touser1
- The attacker gifts
doc1
, changing the owner fromuser1
touser2
- 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 documentdoc1
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 documentrequest.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() })