Clickjacking in Rails with Jwt Tokens
Clickjacking in Rails with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side attack that tricks a user into interacting with a hidden or disguised UI element inside an iframe. When a Rails application uses JWT tokens for authentication but relies solely on cookie-based session handling for rendering frames, the combination can expose the application to clickjacking. A common pattern is to store a JWT in an HttpOnly cookie and use server-side view helpers to decide whether to render frames or embedded content. If these views do not explicitly prevent framing, an attacker can load the authenticated page inside an invisible iframe and overlay interactive controls, leading to unauthorized actions while the JWT is still valid.
Consider a Rails controller that authenticates via a JWT in a cookie and renders a page with sensitive actions such as changing an email or updating financial settings:
class DashboardController < ApplicationController
before_action :authenticate_user_from_jwt
def show
# Renders a page with buttons that perform state-changing operations
end
def change_email
# Performs email update based on params
end
private
def authenticate_user_from_jwt
token = cookies.signed[:jwt]
# Decode and validate token; set current_user or render 401
end
end
If the corresponding view does not set a framing policy, an attacker can craft a page like this:
<!----> Attacker-controlled page
<iframe src="https://app.example.com/dashboard/change_email" style="opacity:0;position:absolute"></iframe>
<button onclick="document.querySelector('iframe').contentDocument.querySelector('form button').click()">Win a prize!</button>
When the victim is logged in and has a valid JWT in a cookie, the hidden iframe submits the change_email action with the user’s authentication context. Rails does not automatically protect against this because the JWT validates identity but does not instruct the browser to block framing. The vulnerability is not in the JWT itself but in the missing anti-framing controls in Rails views and response headers.
Additionally, if the Rails app embeds third-party widgets or iframes and those frames include UI that can trigger authenticated actions, the risk increases. The JWT ensures the request is authenticated, but without proper X-Frame-Options or Content-Security-Policy frame-ancestors directives, the browser will honor the request and render the page inside the attacker’s frame. This demonstrates why authentication and framing policies must be addressed independently.
Jwt Tokens-Specific Remediation in Rails — concrete code fixes
To mitigate clickjacking in a Rails app using JWT tokens, combine secure token handling with HTTP headers and view-level framing controls. JWTs should not be placed in cookies that are automatically sent with cross-site requests unless SameSite and Secure attributes are enforced. Prefer Authorization: Bearer handling for API requests and avoid relying solely on cookies for sensitive actions that may be embedded in third-party frames.
Set X-Frame-Options and Content-Security-Policy headers to prevent framing of authenticated pages:
# config/initializers/frame_protection.rb
Rails.application.config.action_dispatch.default_headers = {
'X-Frame-Options' => 'DENY',
'Content-Security-Policy' => "frame-ancestors 'none'"
}
For pages that must be embedded in a controlled context (e.g., a dashboard widget), use a granular CSP instead of a global DENY:
#' Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com
When handling JWTs in cookies, enforce SameSite and Secure attributes to reduce cross-site leakage:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_app_session',
same_site: :lax,
secure: Rails.env.production?
If you choose to store the JWT in localStorage or another client-side mechanism, ensure your Rails views do not inadvertently expose the token via JavaScript in unsafe ways, and always use CSRF protection for state-changing requests that originate from browser contexts. For API-style endpoints consumed by JavaScript, validate the Authorization header and do not rely on cookie-based authentication alone.
Example of a controller that verifies a JWT from the Authorization header and safely handles sensitive actions:
class Api::V1::ProfilesController < ApplicationController
before_action :authenticate_user_from_bearer_token
def update_email
# Only reachable with a valid bearer JWT
current_user.update!(email_params)
head :no_content
end
private
def authenticate_user_from_bearer_token
token = request.headers['Authorization']&.split(' ')&.last
return head :unauthorized unless token
# Decode and verify; set current_user or render 401
end
def email_params
params.require(:email).permit(:address)
end
end
Combine these practices to ensure that even if a page can be framed, authenticated actions are protected by strict token handling and framing policies. Regularly review CSP reports and test frame embedding scenarios to confirm protections are effective.