Cross Site Request Forgery in Sinatra with Firestore
Cross Site Request Forgery in Sinatra with Firestore — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) in a Sinatra app that uses Google Cloud Firestore arises when an authenticated Firestore session is tied to cookies or headers that the browser automatically sends on cross-origin requests. Without anti-CSRF protections, an attacker can trick a logged-in user’s browser into issuing state-changing Firestore writes through crafted HTML forms or image tags that point to Sinatra routes which mutate data.
Consider a Sinatra route that updates a Firestore document based on user-supplied parameters without verifying request origin:
require 'sinatra'
require 'google/cloud/firestore'
configure do
set :public_folder, File.join(Dir.pwd, 'public')
set :views, File.join(Dir.pwd, 'views')
end
firestore = Google::Cloud::Firestore.new
post '/update_profile' do
user_id = session[:user_id]
bio = params[:bio]
# Dangerous: no CSRF check
firestore.doc("users/#{user_id}").update(bio: bio)
"Profile updated"
end
If an attacker hosts a page with <form action="https://your-sinatra-app/update_profile" method="POST"><input name="bio" value="Hacked"/></form> and uses JavaScript to auto-submit it, the user’s browser may include session cookies (or an auth token) and the request will succeed from the server’s perspective. Because the Sinatra app trusts the session cookie and does not validate the request source, the attacker can change the user’s Firestore document.
The risk is higher when Sinatra uses cookie-based sessions and does not set SameSite attributes or CSRF tokens. Firestore security rules alone cannot prevent this: rules validate data and identity but not request origin. Therefore, the combination of Sinatra routes that perform writes and browser-managed session cookies creates a CSRF surface that must be mitigated at the application layer.
Another scenario involves forms that embed user-controlled values into Firestore document paths or fields without synchronizer tokens. For example, a form that sends project_id and uses it to update projects/:project_id/settings can be exploited if the attacker can predict or harvest a valid project ID from the victim’s session or public data.
Firestore-Specific Remediation in Sinatra — concrete code fixes
To defend against CSRF in Sinatra with Firestore, apply per-request origin verification and anti-CSRF tokens for any route that mutates Firestore documents. Below are concrete, working examples.
1. Use origin and referer checks for safe methods (defense in depth)
helpers do
def safe_request?
allowed_origins = ['https://your-sinatra-app.com']
origin = request.env['HTTP_ORIGIN']
referer = request.env['HTTP_REFERER']
allowed_origins.include?(origin) || (referer && referer.start_with?('https://your-sinatra-app.com'))
end
end
2. Implement per-session CSRF tokens and require them for writes
helpers do
def csrf_token
session[:csrf_token] ||= SecureRandom.base64(32)
end
def verify_csrf_token
halt 403, 'Invalid CSRF token' unless params[:csrf_token] == session[:csrf_token]
end
end
before do
content_type :json
end
post '/update_profile' do
verify_csrf_token
user_id = session[:user_id]
bio = params[:bio]
firestore = Google::Cloud::Firestore.new
firestore.doc("users/#{user_id}").update(bio: bio)
{ status: 'ok' }.to_json
end
get '/form_page' do
erb :update_profile, locals: { csrf_token: csrf_token }
end
In your ERB template (views/update_profile.erb), include the token as a hidden field:
<form action="/update_profile" method="POST">
<input name="bio" type="text" />
<input name="csrf_token" value="<%= csrf_token %>" />
<button type="submit">Save</button>
</form>
3. Enforce SameSite cookies and secure flags
configure do
enable :sessions
set :session_secret, ENV.fetch('SESSION_SECRET') { 'development_secret_change_in_production' }
# Rack cookies options can be set via session options if supported by your adapter
end
Ensure your session cookie attributes include SameSite=Lax or SameSite=Strict and in production. In modern Rack/Sinatra setups this is often configured via the session store or middleware; verify using browser dev tools.
4. For API-driven clients, require custom request headers and check Origin
post '/api/update_settings' do
halt 403 unless request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
halt 403 unless safe_request?
user_id = session[:user_id]
settings = JSON.parse(request.body.read)
firestore = Google::Cloud::Firestore.new
firestore.doc("users/#{user_id}/settings").set(settings, merge: true)
{ status: 'updated' }.to_json
end
These measures ensure that even if a malicious site can make the user’s browser send requests, they will fail CSRF token or origin checks. Remember that CSRF protection complements, but does not replace, Firestore security rules and least-privilege IAM bindings.