HIGH broken access controlrailsfirestore

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.

Frequently Asked Questions

Can Firestore security rules alone protect data in a Rails app?
No. Rules are a last line of defense. Rails must scope reads/writes to the authenticated user and not rely on client-supplied identifiers or assume rules alone prevent unauthorized access.
What is a secure way to reference a user's Firestore document in Rails?
Use the authenticated user's UID to construct document paths server-side, for example: Firestore::DocumentReference.new(collection: 'users', path: "users/#{current_user.uid}/profile"), and avoid using raw client input for document IDs or paths.