Module: ConcernsOnRails::Controllers::SecureHeadable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/controllers/secure_headable.rb

Overview

Adds modern security response headers and wires Rails’ native Content-Security-Policy DSL. Defense-in-depth on top of output escaping —this does NOT scrub request params (context-blind and lossy) and never re-enables the deprecated X-XSS-Protection auditor.

class ApplicationController < ActionController::Base
  include ConcernsOnRails::Controllers::SecureHeadable

  # Apply preset headers, plus any custom "Header-Name" => "value" pairs:
  secure_headers :nosniff, :sameorigin_frame, :no_referrer_leak, :disable_legacy_xss
  secure_headers "Permissions-Policy" => "geolocation=()"

  # Delegates to Rails' native CSP DSL — roll out report-only FIRST:
  content_security_policy_for(report_only: true) do |policy|
    policy.default_src :self
    policy.script_src  :self
    policy.object_src  :none
  end
end

The headers mitigate clickjacking / MIME-sniffing and (via CSP) XSS as defense-in-depth — they are NOT a standalone XSS fix; output escaping remains the primary defense. Per-controller CSP overrides the global initializer for that controller. CSP nonce generation (content_security_policy_nonce_generator / _nonce_directives) is app-wide initializer configuration and intentionally stays out of this concern.

Header presets (the ‘secure_headers` arguments):

:nosniff            — X-Content-Type-Options: nosniff
:sameorigin_frame   — X-Frame-Options: SAMEORIGIN
:deny_frame         — X-Frame-Options: DENY
:no_referrer_leak   — Referrer-Policy: strict-origin-when-cross-origin
:no_cross_domain    — X-Permitted-Cross-Domain-Policies: none
:disable_legacy_xss — X-XSS-Protection: 0 (the only correct modern value)

Constant Summary collapse

PRESETS =

Frozen, string-only header presets, each “Header-Name” => “value”. :disable_legacy_xss emits “0” deliberately — the legacy browser XSS auditor was itself exploitable and is gone from modern browsers (Rails 7+ ships “0”), so “0” is the only correct value.

{
  nosniff: %w[X-Content-Type-Options nosniff],
  sameorigin_frame: %w[X-Frame-Options SAMEORIGIN],
  deny_frame: %w[X-Frame-Options DENY],
  no_referrer_leak: %w[Referrer-Policy strict-origin-when-cross-origin],
  no_cross_domain: %w[X-Permitted-Cross-Domain-Policies none],
  disable_legacy_xss: %w[X-XSS-Protection 0]
}.freeze

Instance Method Summary collapse

Instance Method Details

#apply_secure_headersObject

Public so subclasses can override; guarded exactly like Paginatable so it no-ops cleanly when there is no response object.



98
99
100
101
102
# File 'lib/concerns_on_rails/controllers/secure_headable.rb', line 98

def apply_secure_headers
  return unless respond_to?(:response) && response

  self.class.secure_headable_headers.each { |name, value| response.set_header(name, value) }
end