Search code examples
angularfirebasegoogle-cloud-firestoreangularfire2

Firebase Firestore - Limit the number of product-documents users can create


I have an Angular web-app with Firebase as the serverless backend technology. The current purpose of the web-app is to let registered users create documents for offered products. While business users can create unlimited product-docs, free users should only be able to create one product-doc.

Collections:

  • users - {id: string, isBusiness: boolean, name: string, email: string, productIds: string[]}
  • products - {id: string, title: string, description: string, userId: string}

Whenever a user creates a new product-doc with

  public createProduct(id: string, product: Product) {
    return this.afStore.collection('products').doc(id).set(product, { merge: true });
  }

I run a cloud function with the trigger onCreate to add the id of the created product-doc to the productIds field of the according user-doc. This allows me to check frontend-side if(!user.isBusiness && productIds.length > 1) to limit the creation of multiple product-docs for free user accounts.

However, I recently did a pen-test and the testers accidentality found out that if you open the website on multiple browser tabs and simultaneously run the createProduct() function on each tab at the same time, all of them will execute allowing a non business user to have several product-docs.

Has anybody dealt with a similar situation? I don´t know if there is a way to avoid this with Firestore Security Rules, or if there is something to wait until active transactions are finished.


Solution

  • The current implementation suffers from a race condition: A 'non-business' user may create an arbitrary number of products until the first productId is written to the user doc.

    In my eyes, the most straightforward way to solve this, is to pre-populate the non-business users with a productId, and restrict their ability to assign new ones.

    • When the non-business user is created, you create/reserve a unique productId for them, which they may or may not use. Store it in the user document.
    • When a non-business user creates a product, they can only write data to that one productId. While not needed to fix the described use case, depending on your security requirements you may want to enforce this through a security rule or a cloud function.
    • No matter how many concurrent tabs try to create products, they will always write to the same productId.
    public createProduct(id: string, product: Product, user: User) {
        if (user.isBusiness) {
          return this.afStore.collection('products').doc(id).set(product, { merge: true });
        } else {
          return this.afStore.collection('products').doc(user.reservedProductId).set(product, { merge: true });
        }    
    }
    

    Caveat: As with any frontend code, this will not prevent malicious actors from manipulating it to their liking. If this behaviour is a security concern, you will want to restrict write access to the products collection by non-business users. Consider implementing an appropriate security rule / cloud function. You can only trust the backend.