Brute Force Attack in Rails with Firestore
Brute Force Attack in Rails with Firestore — how this specific combination creates or exposes the vulnerability
A brute force attack against a Ruby on Rails application that uses Google Cloud Firestore typically targets authentication or password reset endpoints. Because Firestore is often used as a user store or a session/state store, Rails controllers that validate credentials against Firestore can become the effective gate for account access.
Without proper rate controls, each login attempt can map to a Firestore document read (e.g., querying by email). An attacker can iterate over many usernames or passwords, generating high read operations that may be observable via logs or monitoring. While Firestore enforces its own quotas, the application layer must enforce rate limits to prevent rapid, unthrottled requests that constitute a brute force attempt.
Common insecure patterns in Rails include using User.where(email: params[:email]).first directly against the Firestore-backed model without any exponential backoff or captcha, and returning distinct error messages (e.g., “email not found” vs “incorrect password”), which enables username enumeration. The Rails session store or token verification logic that reads from Firestore may also be abused if tokens or API keys are guessable or leaked, effectively turning Firestore into a persistence mechanism for attack state.
Because Firestore queries are predictable and index-backed, attackers can probe for valid emails or use automated scripts to attempt many combinations. If the Rails app does not enforce per-identity or per-IP rate limits, the unauthenticated attack surface expands. This risk is amplified when Firestore rules allow public read or when indices inadvertently expose user collections.
middleBrick’s 12 security checks include Rate Limiting and Authentication, which can detect missing or weak controls around login endpoints that rely on Firestore. The scanner tests unauthenticated attack surfaces and can identify whether brute force protections such as account lockout, delays, or captchas are absent, providing prioritized findings with severity and remediation guidance.
Firestore-Specific Remediation in Rails — concrete code fixes
Implementing robust brute force defenses in Rails with Firestore requires both application-level controls and secure usage of the Firestore client. Below are concrete, realistic examples.
1. Rate-limited sign-in with constant-time response
Use a cache-based rate limiter (e.g., Redis or ActiveSupport::Cache) to enforce per-email and per-IP limits, and return a generic, consistent message to avoid enumeration.
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
RATE_LIMIT = 5 # attempts
WINDOW = 5.minutes
def create
email = params.dig(:user, :email)&.strip&.downcase
ip = request.remote_ip
cache_key_email = "brute:email:#{email}"
cache_key_ip = "brute:ip:#{ip}"
if increment_and_check(cache_key_email, RATE_LIMIT, WINDOW) ||
increment_and_check(cache_key_ip, RATE_LIMIT, WINDOW)
# Always perform the same work and delay to prevent timing leaks
User.dummy_verify(email, params[:password])
render json: { error: 'Invalid credentials' }, status: :unauthorized
return
end
user = User.find_by_email(email)
if user&.authenticate(params[:password])
reset_access_attempts(cache_key_email, cache_key_ip)
session[:user_id] = user.id
render json: { status: 'ok' }, status: :ok
else
# Constant-time dummy verification to obscure timing
User.dummy_verify(email, params[:password])
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
private
def increment_and_check(key, limit, window)
count = $cache.read(key, raw: true) || 0
if count >= limit
true
else
$cache.write(key, count + 1, expires_in: window, raw: true)
false
end
end
def reset_access_attempts(email_key, ip_key)
$cache.delete(email_key)
$cache.delete(ip_key)
end
end
2. Secure Firestore client usage in the User model
Use the Firestore Ruby client safely by avoiding public rules and parameterizing queries. Do not expose Firestore errors that reveal existence of an email.
# app/models/user.rb
class User < ApplicationRecord
# Assume Firestore is initialized via a service object or initializer
def self.firestore
@firestore ||= Google::Cloud::Firestore.new
end
def self.find_by_email(email)
doc_ref = firestore.doc("users/#{email}")
# Use a secure document read; ensure rules require authentication for sensitive reads
doc = doc_ref.get
doc && doc.exists? ? from_firestore_doc(doc) : nil
end
def self.from_firestore_doc(doc)
attrs = doc.data
new(
id: doc.document_id,
email: attrs["email"],
password: attrs["password_hash"]
)
end
def authenticate(password_attempt)
# Use a constant-time comparison where feasible
return false unless password_hash
ActiveSupport::SecurityUtils.secure_compare(password_hash, BCrypt::Password.create(password_attempt, cost: Rails.application.config.bcrypt_cost))
end
def self.dummy_verify(email, password_attempt)
# Always run a dummy hash operation to obscure timing differences
BCrypt::Password.create("dummy_salt_#{email}") if password_attempt.present?
end
end
3. Firestore security rules to enforce least privilege
Ensure rules do not allow public enumeration. For example, disallow list operations and require authentication for user-specific reads/writes.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{email} {
allow read, write: if request.auth != null && request.auth.token.email == email;
deny list: if true;
}
}
}
4. MiddleBrick integrations for continuous protection
With the Pro plan, you can enable continuous monitoring so that any regression in rate limiting or authentication configuration is caught early. Add the middleBrick GitHub Action to fail builds if the security score drops below your chosen threshold, or use the CLI to scan from the terminal: middlebrick scan <url>. The MCP Server also lets you scan APIs directly from your AI coding assistant within the Rails project.