Cross Site Request Forgery in Grape with Basic Auth
Cross Site Request Forgery in Grape with Basic Auth — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) is an attack where a victim is tricked into executing unwanted actions on a web application where they are authenticated. When using Basic Authentication in a Grape API, the risk profile changes compared to cookie-based sessions. Basic Auth sends credentials with each request via the Authorization header (e.g., Authorization: Basic base64(username:password)). If a victim’s browser automatically includes those credentials—such as when the user is logged in to the API’s host domain and visits a malicious site—the browser may send the Authorization header to your Grape endpoint during a forged request from the malicious site. This can occur with state-changing HTTP methods like POST, PUT, PATCH, or DELETE, especially if the endpoint does not require a CSRF token or a same-site cookie policy to mitigate cross-origin authorization.
Grape APIs are often used as backend services consumed by web frontends or mobile apps. If your Grape endpoints are called from browsers and rely solely on Basic Auth without additional anti-CSRF controls, an attacker can craft an HTML form or script on a malicious site that triggers requests to your API. Because the browser automatically attaches the Basic Auth credentials (if the request URL matches the host and the user has an active session), the server may process the request as authenticated. This is particularly relevant when CORS is misconfigured to allow credentials, or when preflight checks are too permissive, allowing unsafe methods or headers from untrusted origins.
Consider a Grape endpoint that transfers funds:
post '/transfer' do
account_from = params[:from]
account_to = params[:to]
amount = params[:amount]
# ... transfer logic
end
If this endpoint uses only Basic Auth and accepts POST requests from any origin with credentials, an attacker can host a page containing:
<form action="https://api.example.com/transfer" method="POST">
<input type="hidden" name="from" value="attacker_account">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="9999">
<input type="submit" value="Click me">
</form>
<script>document.forms[0].submit();</script>
If the victim’s browser has cached or is actively sending Basic Auth credentials for api.example.com, the request will be processed as if initiated by the victim. The lack of a same-site policy or CSRF token allows the forged request to succeed. Unlike cookie-based authentication, Basic Auth does not rely on cookies, so standard SameSite cookie protections do not apply; mitigation must be implemented at the API layer.
Basic Auth-Specific Remediation in Grape — concrete code fixes
To mitigate CSRF when using Basic Auth in Grape, you should avoid relying on browser behavior and enforce explicit anti-CSRF controls for state-changing requests. Since Basic Auth credentials are sent with every request to the host, you must ensure that requests originate from your own frontend. Use the following approaches in your Grape API:
- Require a custom header (e.g., X-Requested-With or X-CSRF-Token) for any non-GET request. Browsers do not allow cross-origin JavaScript to set custom headers due to CORS, so a forged form or script cannot include this header.
- Validate the Origin header for sensitive endpoints, ensuring requests come from your trusted frontend origins.
- Use CORS correctly: do not set Access-Control-Allow-Credentials for untrusted origins, and restrict Access-Control-Allow-Methods and Access-Control-Allow-Headers to only what you need.
Example of a Grape endpoint requiring a custom CSRF header:
before do
if %w[POST PUT PATCH DELETE].include?(request.request_method)
halt 403, { error: 'Forbidden' }, { 'Content-Type' => 'application/json' } unless env['HTTP_X_CSRF_TOKEN'] == expected_csrf_value
end
end
post '/transfer' do
account_from = params[:from]
account_to = params[:to]
amount = params[:amount]
# ... transfer logic
end
Example of validating the Origin header:
helpers do
def validate_origin
allowed_origins = ['https://your-frontend.com']
origin = request.env['HTTP_ORIGIN']
unless allowed_origins.include?(origin)
halt 403, { error: 'Invalid origin' }
end
end
end
before { validate_origin if %w[POST PUT PATCH DELETE].include?(request.request_method) }
When integrating with frontends, ensure your JavaScript sets the custom header on every state-changing request:
fetch('https://api.example.com/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': 'a-secure-random-token-from-your-frontend'
},
body: JSON.stringify({ from: 'user_account', to: 'recipient', amount: 100 })
});
Additionally, consider using the middleBrick CLI to scan your Grape endpoints for CSRF and other misconfigurations. Run middlebrick scan <url> to get a security risk score and findings, including CSRF-related issues, with prioritized remediation guidance. For teams needing continuous monitoring, the Pro plan provides scheduled scans and integration options to fit into your workflow.