Clickjacking in Hanami with Bearer Tokens
Clickjacking in Hanami with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side UI redress attack where an attacker tricks a user into interacting with a hidden or disguised element inside an invisible or disguised frame. In Hanami, a Ruby web framework, including a Bearer token in the Authorization header does not protect a page from being embedded in an <iframe> or <object> on a malicious site. The token is sent automatically by the browser with any same-site credentials, so a privileged action protected only by token presence can be invoked without the user’s consent when the malicious page triggers a form submission or a JavaScript-driven request.
For example, suppose a Hanami app exposes an endpoint that accepts DELETE or POST requests and relies solely on an Authorization: Bearer header for authorization. If the response does not set an appropriate X-Frame-Options or Content-Security-Policy frame-ancestors directive, an attacker can craft a page that embeds the endpoint URL inside a hidden form or image request. When a logged-in user with a valid Bearer token visits the attacker’s page, the browser sends the token with the forged request. Because the token is included automatically, the server may process the action as intended by the legitimate user, leading to unauthorized state changes such as changing email, updating settings, or deleting resources.
To illustrate, consider a Hanami controller that performs a sensitive update:
class Web::Controllers::Account::UpdateEmail
include Web::Action
def call(params)
if auth_header = env['HTTP_AUTHORIZATION']&.then { |h| h.split(' ').last }
# Verify Bearer token and perform update
if current_user.update(email: params[:email])
Response.new.status = 200
else
Response.new.status = 422
end
else
Response.new.status = 401
end
end
end
If this endpoint does not enforce strict anti-clickjacking protections, an attacker can host a page with:
<!DOCTYPE html>
<html>
<body>
<form action="https://api.example.com/account/update-email" method="POST" style="display:none;">
<input type="email" name="email" value="[email protected]" />
<input type="submit" value="Click Me" />
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
Even with a Bearer token, the browser includes the Authorization header if the token was issued for the same domain and the credentials mode permits it. Without explicit anti-framing controls, the server may mistakenly treat the forged request as legitimate. This shows why security in Hanami must include both proper authorization via Bearer tokens and robust UI-layer defenses against embedding.
Bearer Tokens-Specific Remediation in Hanami — concrete code fixes
Remediation in Hanami requires a defense-in-depth approach: enforce anti-framing headers, use secure cookie attributes if sessions are involved, and ensure that sensitive actions require explicit user intent beyond the presence of a Bearer token. Below are concrete, syntactically correct examples tailored to a Hanami application.
1) Set anti-framing headers globally in your application controller to prevent embedding:
class Web::ApplicationController < Hanami::Action
before do
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
end
end
2) Require additional confirmation for sensitive actions (e.g., email change) by validating a same-site cookie or a per-request CSRF-like token, even when using Bearer authentication. For APIs that are truly token-only and meant for non-browser clients, ensure that sensitive endpoints reject requests that include cookie headers or ambiguous origins:
class Web::Controllers::Account::UpdateEmail
include Web::Action
def call(params)
auth_header = env['HTTP_AUTHORIZATION']&.then { |h| h.split(' ').last }
return halt(401, { error: 'Unauthorized' }.to_json) unless auth_header && valid_token?(auth_header)
# Reject requests that include cookie headers in API contexts to reduce confusion-based attacks
if env['HTTP_COOKIE']
return halt(400, { error: 'Unexpected credentials' }.to_json)
end
# Require a JSON body parameter that is not auto-sent by browsers in cross-origin frames
email = params[:email]
return halt(422, { error: 'Missing email' }.to_json) unless email&.match?(URI::MailTo::EMAIL_REGEXP)
if current_user.update(email: email)
Response.new.status = 200
else
Response.new.status = 422
end
end
private
def valid_token?(token)
# Implement your token validation logic (e.g., JWT verification or DB lookup)
token == 'expected_secure_token_value'
end
end
3) For browser-based clients that use cookies alongside tokens, separate concerns clearly and avoid relying on cookies for authorization when Bearer tokens are the primary mechanism:
# config/initializers/session_store.rb
Hanami::Web::App.configure do
use Rack::Session::Cookie, key: '_hanami_session', secure: true, httponly: true, same_site: :strict
end
4) When serving pages that include sensitive actions, explicitly require a same-site cookie or a custom header that cannot be set cross-origin:
class Web::Controllers::Sensitive::FormPage
include Web::Action
def call(params)
# Require a custom header set by your frontend JavaScript running on the trusted origin
expected = env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
return halt(403) unless expected
# Set anti-framing headers for this page
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
# Render your form page...
end
end
These measures ensure that even if a Bearer token is present, the browser cannot silently trigger unauthorized actions from a malicious site, mitigating clickjacking risks specific to token-based authentication in Hanami.