Summary
The leaderboard/{userId} Firestore security rule contains an allow write clause that grants any authenticated user full write access to their own leaderboard document. Because Firestore evaluates allow statements with OR logic, the broader allow write check matches before the narrower field-restricted allow update check is ever reached, making the latter completely ineffective.
Rule as written
match /leaderboard/{userId} {
allow read: if true;
allow write: if (request.auth != null && request.auth.uid == userId) || isSuperAdmin();
allow update: if request.auth != null && request.auth.uid == userId && (
request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points'])
);
}
allow write covers create, update, and delete. Because it is present alongside allow update, Firestore checks whether any matching rule permits the operation. For an update request by the document owner the allow write clause is sufficient on its own, so the field-restricted allow update clause is unreachable and provides no protection.
Impact
- Any authenticated user can set their own
points field to an arbitrary value (e.g., 99999) by sending a direct Firestore write, completely bypassing all server-side score computation.
- The
allow write also permits create and delete, meaning a user can delete their own leaderboard document or create it with fabricated data before the backend ever writes it.
- Leaderboard integrity is fully compromised for any user willing to write directly to Firestore.
Suggested Fix
Remove the broad allow write clause and replace it with explicit, field-restricted rules for each operation:
match /leaderboard/{userId} {
allow read: if true;
// Only the backend (via Admin SDK) or a superadmin should create or delete entries.
allow create, delete: if isSuperAdmin();
// Authenticated users may only update fields the server legitimately writes.
// Remove this clause entirely if all leaderboard writes should be server-only.
allow update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['points']);
}
Ideally all leaderboard mutations should flow through a trusted backend (Cloud Functions or Admin SDK) with no direct client write access at all. That is the most robust solution.
Steps to Reproduce
- Sign in to the app as any regular user.
- Open the browser console or a REST client and send a Firestore
update or set call to leaderboard/<your-uid> with { points: 99999 }.
- Observe that the write succeeds and the leaderboard reflects the fabricated score.
Summary
The
leaderboard/{userId}Firestore security rule contains anallow writeclause that grants any authenticated user full write access to their own leaderboard document. Because Firestore evaluatesallowstatements with OR logic, the broaderallow writecheck matches before the narrower field-restrictedallow updatecheck is ever reached, making the latter completely ineffective.Rule as written
allow writecoverscreate,update, anddelete. Because it is present alongsideallow update, Firestore checks whether any matching rule permits the operation. For an update request by the document owner theallow writeclause is sufficient on its own, so the field-restrictedallow updateclause is unreachable and provides no protection.Impact
pointsfield to an arbitrary value (e.g.,99999) by sending a direct Firestore write, completely bypassing all server-side score computation.allow writealso permitscreateanddelete, meaning a user can delete their own leaderboard document or create it with fabricated data before the backend ever writes it.Suggested Fix
Remove the broad
allow writeclause and replace it with explicit, field-restricted rules for each operation:Ideally all leaderboard mutations should flow through a trusted backend (Cloud Functions or Admin SDK) with no direct client write access at all. That is the most robust solution.
Steps to Reproduce
updateorsetcall toleaderboard/<your-uid>with{ points: 99999 }.