Clickjacking in Rails with Api Keys
Clickjacking in Rails with Api Keys — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side attack where an invisible or misleading UI element tricks a user into performing unintended actions. In a Ruby on Rails application that uses API keys for external service authorization, combining clickjacking with key exposure can lead to unauthorized actions and credential compromise. When Rails views render third-party content or embed external endpoints inside iframes, an attacker can overlay transparent controls that capture clicks meant for legitimate UI elements. If the current session holds API keys—whether in headers, cookies, or JavaScript variables—an inadvertent click can propagate those keys to malicious destinations.
Rails applications often manage API keys through environment variables or encrypted credentials, but accidental leakage commonly occurs in logs, error pages, or front-end code. For example, embedding a third-party analytics dashboard inside an iframe may expose the host page’s context, including authenticated headers that contain API keys. If the application uses key-based authentication for outbound HTTP requests, an attacker who can drive user clicks might indirectly cause the victim’s browser to issue requests that include those keys. This becomes particularly dangerous when Rails helpers or JavaScript templates serialize API keys into data attributes or meta tags, making them accessible to scripts running in the context of an embedded resource.
The risk is amplified when Rails views render content that includes user-controlled URLs without proper isolation. If an application provides a feature to preview external URLs via an <iframe> or uses JavaScript to dynamically set src attributes, an attacker can craft a malicious page that loads the Rails view inside a nested frame. With Rails’ default protection against clickjacking not explicitly enforced—such as missing X-Frame-Options or Content-Security-Policy frame-ancestors directives—an embedded page can be framed freely. If the Rails view also includes API keys in request headers (added by middleware or HTTP client wrappers), those keys can be presented to the malicious frame’s origin when the user interacts with the page, enabling key exfiltration or unauthorized API calls.
Consider a Rails service that uses HTTParty or Faraday to call a payment provider, attaching an API key in the Authorization header. If a controller action that performs a payment also renders a page with an embedded iframe pointing to a third-party report, and that iframe is controllable by an attacker, the payment action can be triggered via a clickjacked UI. Even if the API key is not directly exposed in the DOM, the session’s authorization headers can be sent automatically by the browser to the Rails endpoint, allowing the attacker to induce the server to make privileged outbound requests. This illustrates how clickjacking vectors in Rails can leverage API key–based authentication to escalate impact beyond the web interface.
Api Keys-Specific Remediation in Rails — concrete code fixes
Remediation focuses on preventing unauthorized framing and minimizing API key exposure in client-side contexts. Start by enforcing frame-ancestors policies and ensuring API keys never reach the browser. Below are concrete Rails patterns to achieve this.
1. Enforce X-Frame-Options and Content-Security-Policy
Configure your application to disallow embedding in iframes. In config/application.rb or an environment-specific initializer, set headers that protect against clickjacking targeting API key–bearing views.
# config/application.rb or a concern in app/controllers/concerns/frame_protection.rb
class FrameProtection
def self.included(base)
base.before_action :set_frame_options
end
private
def set_frame_options
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
end
end
Include this concern in controllers that handle sensitive actions, such as those invoking external APIs with keys.
2. Avoid exposing API keys in views and JavaScript
Never assign API keys to instance variables that are rendered in ERB or accessible via JavaScript. Instead, keep keys server-side and inject only necessary tokens or non-sensitive identifiers to the front end.
# app/controllers/api_controller.rb
class ApiController < ApplicationController
before_action :authenticate_user!, :set_backend_headers
def show
# Perform server-side request using stored key; do not pass key to view
result = ExternalService.call(user: current_user)
render json: { data: result.sensitive_data }
end
private
def set_backend_headers
@backend_headers = {
'Authorization' => "Bearer #{ENV['EXTERNAL_API_KEY']}"
}
end
end
3. Use form_with and authenticity token for state-changing actions
Ensure that any form or AJAX request that triggers API-key–driven operations is protected by Rails’ authenticity mechanisms. Do not rely on GET requests for mutations that involve secret keys.
# app/views/payments/_form.html.erb
<%= form_with(model: Payment.new, local: true) do |f| %>
<%= f.hidden_field :nonce, value: SecureRandom.uuid %>
<%= f.submit 'Process Payment' %>
<%= end %>
4. Sanitize and validate externally supplied URLs
If your Rails app accepts URLs for previews or webhooks, validate and isolate them to prevent clickjacking via nested frames. Use a strict allowlist and avoid rendering user URLs directly in iframes.
# app/validators/url_validator.rb
class UrlValidator < ActiveModel::EachValidator
ALLOWED_DOMAINS = %w[api.partner.com secure.example.org]
def validate_each(record, attribute, value)
uri = URI.parse(value)
unless ALLOWED_DOMAINS.include?(uri.host)
record.errors.add(attribute, 'domain not allowed')
end
rescue URI::InvalidURIError
record.errors.add(attribute, 'invalid URL')
end
end
5. Rotate keys and monitor usage
Even with strong framing controls, treat API keys as potentially exposed. Rotate keys regularly and implement monitoring on the provider side to detect anomalous usage patterns triggered by clickjacked interactions.