Cross Site Request Forgery in Sinatra with Bearer Tokens
Cross Site Request Forgery in Sinatra with Bearer Tokens
Cross Site Request Forgery (CSRF) in Sinatra when Bearer Tokens are used involves a mismatch between token-based authentication and the browser’s cookie-based session handling. Bearer tokens are typically stored in client-side JavaScript or browser storage and sent via the Authorization header. When a browser automatically includes cookies (e.g., session or CSRF tokens) but the backend relies solely on the Authorization header, an attacker can craft a form on a malicious site that triggers state-changing requests. The browser will include cookies automatically, but the missing or incorrect CSRF protection on the API endpoint can allow the request to succeed if the token is valid and not properly validated against the origin.
In Sinatra, a common pattern is to authenticate requests using a before filter that reads the Authorization header and validates the Bearer token. However, if the application does not verify the request origin or enforce explicit CSRF defenses for state-changing methods (POST, PUT, PATCH, DELETE), an attacker can trick a logged-in user’s browser into submitting a request like:
POST /api/transfer HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMSJ9.xxxxx
Content-Type: application/x-www-form-urlencoded
amount=1000&to=attacker
If the Sinatra route trusts the Authorization header and does not validate the Origin or Referer headers, or does not require an explicit anti-CSRF token in the request body or header, the server may process the transfer. This occurs because the browser sends cookies (if any are present) and the Authorization header together, and the server conflates a valid token with a legitimate user intent. The risk is elevated when tokens are long-lived or when the API serves both web and non-web clients without clear separation.
CSRF against Bearer-token APIs is not automatic; it requires the attacker to know or guess a valid token or to leverage a token that is accidentally exposed to the browser. If tokens are stored in HttpOnly, Secure cookies and not accessible to JavaScript, direct CSRF via injected forms is less likely. However, if the token is exposed to JavaScript (e.g., stored in localStorage and added to Authorization headers manually), an XSS flaw can complement CSRF to achieve a more severe impact. Therefore, treating Bearer-token endpoints as CSRF-protected by default is unsafe in Sinatra without explicit mitigations.
Bearer Tokens-Specific Remediation in Sinatra
To secure Sinatra endpoints that use Bearer tokens, implement explicit CSRF protections tailored to token-based flows. The goal is to ensure that requests with a Bearer token are not inadvertently accepted from untrusted origins. Below are concrete, actionable fixes with code examples.
- Validate the Origin and Referer headers for state-changing requests. Reject requests where the origin does not match your trusted domain:
require 'sinatra'
require 'json'
helpers do
def authorized?(token)
# Replace with your token validation logic
token == 'VALID_TOKEN_EXAMPLE'
end
def allowed_origin?
origin = request.env['HTTP_ORIGIN']
referer = request.env['HTTP_REFERER']
trusted = 'https://yourtrusted.com'
origin == trusted || referer&.start_with?(trusted)
end
end
before do
# Skip CSRF checks for safe methods
return if ['GET', 'HEAD', 'OPTIONS'].include?(request.request_method)
auth = request.env['HTTP_AUTHORIZATION']
token = auth&.split&.last
halt 401, { error: 'Unauthorized' }.to_json unless token && authorized?(token)
halt 403, { error: 'Forbidden: Invalid origin' }.to_json unless allowed_origin?
end
post '/api/transfer' do
content_type :json
{ status: 'ok' }.to_json
end
- Use per-route or per-method CSRF tokens when the API serves browser-originated clients. Require a custom header (e.g., X-CSRF-Token) that matches a server-side value:
enable :sessions
get '/api/csrf_token' do
session[:csrf_token] ||= SecureRandom.hex(32)
{ csrf_token: session[:csrf_token] }.to_json
end
post '/api/action' do
auth = request.env['HTTP_AUTHORIZATION']
token = auth&split&.last
halt 401, { error: 'Unauthorized' }.to_json unless token && authorized?(token)
expected = session[:csrf_token]
actual = request.env['HTTP_X_CSRF_TOKEN']
halt 403, { error: 'Invalid CSRF token' }.to_json unless expected && secure_compare(expected, actual)
content_type :json
{ status: 'success' }.to_json
end
def secure_compare(a, b)
return false if a.nil? || b.nil? || a.bytesize != b.bytesize
l = a.unpack 'H*'
r = b.unpack 'H*'
return false unless l && r && l.first.bytesize == r.first.bytesize
# Constant-time comparison to avoid timing attacks
(l.first ^ r.first).hexzero?
end
- For APIs consumed only by non-browser clients or mobile apps, disable CSRF protections selectively and document this clearly. Use a configuration flag to skip origin checks when necessary, but ensure tokens remain in the Authorization header and are not stored in cookies without HttpOnly and Secure flags.
configure :production do
set :csrf_protected, true
end
before do
return if ['GET', 'HEAD', 'OPTIONS'].include?(request.request_method)
# Only enforce CSRF for web-originated traffic
if settings.csrf_protected && !allowed_origin?
halt 403, { error: 'CSRF protection failed' }.to_json
end
# Bearer token validation as above
end