Broken Access Control in Rails with Firestore
Broken Access Control in Rails with Firestore — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when an application fails to enforce proper authorization checks, allowing an authenticated user to access resources or perform actions they should not be permitted to. In a Rails application using Google Cloud Firestore as the backend, the risk is elevated when security rules and server-side authorization are treated as optional or are inconsistently applied. Firestore operates with a permission model defined by rules and relies on the client to pass user context (such as UID) that the rules can evaluate. If the Rails backend does not independently verify whether the requesting user is allowed to access a specific document, an attacker can manipulate requests to access or modify other users' data.
Consider a typical Rails controller action that retrieves a user profile document by ID directly from the client-supplied parameter. If the action builds a Firestore document reference using the ID provided (e.g., params[:profile_id]) and returns the document contents without confirming that the authenticated user owns that document, the application exposes a BOLA/IDOR (Broken Level Authorization / Insecure Direct Object Reference). Firestore rules may restrict reads to documents where request.auth != null and perhaps check a field like user_id == request.auth.uid, but if the Rails server constructs the query or document reference using only client input, the server may bypass intended scoping, effectively acting as an unauthenticated proxy or exposing data across tenants.
Additionally, Firestore rules are evaluated at the database level per request. If the Rails backend uses a service account with elevated privileges to perform reads or writes on behalf of users, and the backend does not enforce user-level authorization, the service account’s permissions become a broad attack surface. For example, an attacker who compromises a low-privilege user session might leverage the Rails backend to request documents belonging to other users, assuming the backend does not validate ownership. A concrete example is a controller action that calls Firestore::DocumentReference.new(collection: "profiles", path: "users/#{params[:user_id]}/profile").get without ensuring the current user matches params[:user_id]. This can lead to mass data exposure if ID values are sequential or easily guessed.
Common real-world patterns that exacerbate the issue include using Firestore query cursors or offsets supplied directly from the client, failing to scope queries by the authenticated user’s ID, and trusting Firestore rules alone when the backend also acts as an intermediary. Attack patterns such as IDOR, privilege escalation via overprivileged service accounts, and unsafe consumption of user-supplied document paths can all manifest in this setup. Findings from scans often highlight missing property authorization and unsafe consumption when Rails endpoints interact with Firestore without strict ownership checks.
Firestore-Specific Remediation in Rails — concrete code fixes
Remediation focuses on ensuring every Firestore access in Rails is scoped to the authenticated user and validated server-side. Never rely solely on Firestore security rules to protect data when your backend intermediates requests. Always resolve the document reference or construct queries using the authenticated user’s identifier, not client-provided identifiers that can be tampered with.
Example: a secure profile retrieval endpoint that uses the current user’s UID from the session or token to build the document path, rather than trusting params[:user_id]. This ensures the server enforces ownership regardless of what the client sends.
# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
before_action :authenticate_user!
def show
user_id = current_user.uid # or however you derive the user identifier
doc_ref = Firestore::DocumentReference.new(
collection: "users",
path: "users/#{user_id}/profile"
)
snapshot = doc_ref.get
if snapshot.exists?
render json: snapshot.data
else
render json: { error: "not found" }, status: :not_found
end
end
end
When querying collections to list user-owned documents, scope the query by the authenticated UID and avoid exposing internal document IDs directly to the client. If you must accept an identifier, map it to the authenticated user on the server before constructing any Firestore reference or query.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
before_action :authenticate_user!
def index
user_id = current_user.uid
messages_ref = Firestore::CollectionReference.new(
collection: "users/#{user_id}/messages"
)
docs = messages_ref.get_documents
render json: docs.map(&:data)
end
end
For operations that involve client-supplied document paths (e.g., from a Firestore query cursor), validate that the resolved path belongs to the authenticated user. One approach is to extract the UID from the path and compare it with the current user’s UID before proceeding. This mitigates risks around unsafe consumption and helps prevent attackers from walking through document hierarchies.
# app/services/firestore_validator.rb
class FirestoreValidator
def self.ensure_user_owns_document(user_id, path)
# Expect paths like "users/USER_ID/collection/..."
unless path.start_with?("users/#{user_id}/")
raise SecurityError, "Access denied: path does not belong to user"
end
end
end
# Usage in controller
class SharedController < ApplicationController
before_action :authenticate_user!
def access_shared
user_id = current_user.uid
requested_path = params[:document_path]
FirestoreValidator.ensure_user_owns_document(user_id, requested_path)
doc_ref = Firestore::DocumentReference.new(path: requested_path)
data = doc_ref.get.data
render json: data
end
end
Additionally, review Firestore security rules to ensure they align with the server-side checks. Rules should still enforce request.auth != null and validate that request.auth.uid == request.resource.data.user_id on writes. However, treat rules as a last line of defense, not the primary enforcement mechanism when Rails intermediates requests.
For applications using the Pro plan, continuous monitoring can help detect anomalous access patterns across Firestore endpoints, while the CLI allows quick scans from the terminal to verify that endpoints conform to secure scoping practices. These tools complement secure coding by providing visibility, but they do not replace correct server-side authorization logic.