Skip to content

Firestore leaderboard rule allows authenticated users to self-report arbitrary point values #348

@anshul23102

Description

@anshul23102

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

  1. Sign in to the app as any regular user.
  2. Open the browser console or a REST client and send a Firestore update or set call to leaderboard/<your-uid> with { points: 99999 }.
  3. Observe that the write succeeds and the leaderboard reflects the fabricated score.

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions