From e31367ad65bfbf44602c1d2100283ce6dfcc6e17 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Fri, 29 May 2026 15:48:16 +0530 Subject: [PATCH] fix(#348): remove broad allow write from leaderboard Firestore rule The leaderboard/{userId} rule had both an allow write and an allow update clause. Firestore evaluates allow statements with OR logic, so the broader allow write was always matched first for document owners. This rendered the field-restricted allow update completely unreachable, letting any authenticated user overwrite their own points with an arbitrary value. Changes: - Removed the allow write clause entirely. - Added allow create, delete: if isSuperAdmin() so only the trusted backend can provision or remove leaderboard entries. - Kept allow update scoped to the document owner with an affectedKeys check restricting writes to the points field only. Fixes #348 --- firestore.rules | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/firestore.rules b/firestore.rules index 9e80a145..3dcea201 100644 --- a/firestore.rules +++ b/firestore.rules @@ -113,12 +113,20 @@ service cloud.firestore { } // Leaderboard collection + // The broad allow write clause has been removed. It was evaluated first by + // Firestore's OR logic, making the field-restricted allow update below it + // completely unreachable, and letting any owner set arbitrary point values. match /leaderboard/{userId} { allow read: if true; - allow write: if (request.auth != null && request.auth.uid == userId) || isSuperAdmin(); // Owner or Super Admin can write - allow update: if request.auth != null && request.auth.uid == userId && ( - (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points'])) - ); + + // Only a superadmin (backend / Admin SDK) may create or delete entries. + allow create, delete: if isSuperAdmin(); + + // Owners may only touch the 'points' field. All other leaderboard + // mutations must go through the trusted backend. + allow update: if request.auth != null + && request.auth.uid == userId + && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points']); } // Resources collection