From 50330f68671309270a08a04d72e0b51240a17740 Mon Sep 17 00:00:00 2001 From: bponders12-cell Date: Tue, 1 Sep 2026 17:07:07 -0400 Subject: [PATCH] SECURITY: enforce profile ownership and immutable identity fields --- firestore.rules | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/firestore.rules b/firestore.rules index 2c23029..88a409e 100644 --- a/firestore.rules +++ b/firestore.rules @@ -9,10 +9,33 @@ service cloud.firestore { return signedIn() && request.auth.uid == userId; } - // Every user-owned document is isolated by the Firebase UID. Cloudflare R2 - // is protected separately by the Worker using the same Firebase ID token. + function validUserCreate(userId) { + return ownsUser(userId) + && request.resource.data.id == userId + && request.resource.data.keys().hasOnly([ + 'id', 'username', 'displayName', 'profilePhotoUrl', 'bio', + 'location', 'createdAt', 'updatedAt', 'privacySettings', 'preferences' + ]); + } + + function validUserUpdate(userId) { + return ownsUser(userId) + && request.resource.data.id == resource.data.id + && request.resource.data.createdAt == resource.data.createdAt + && request.resource.data.keys().hasOnly([ + 'id', 'username', 'displayName', 'profilePhotoUrl', 'bio', + 'location', 'createdAt', 'updatedAt', 'privacySettings', 'preferences' + ]); + } + + // Firebase UID is the security boundary. Profile fields are explicitly + // allow-listed so clients cannot turn the account document into a secret + // or entitlement store. match /users/{userId} { - allow read, write: if ownsUser(userId); + allow create: if validUserCreate(userId); + allow read: if ownsUser(userId); + allow update: if validUserUpdate(userId); + allow delete: if ownsUser(userId); match /rides/{rideId} { allow read, write: if ownsUser(userId); @@ -25,7 +48,8 @@ service cloud.firestore { } match /motorcycles/{motorcycleId} { - allow read, write: if ownsUser(userId); + allow read, write: if ownsUser(userId) + && request.resource.data.ownerId == userId; } match /trips/{tripId} { @@ -41,8 +65,8 @@ service cloud.firestore { } } - // Future public/group collections must be added with explicit field-level - // authorization. Do not use a global recursive wildcard here. + // Future collaborative resources must get explicit membership/field-level + // authorization before they are exposed. match /groupRides/{groupRideId} { allow read, write: if false; }