Class: Tina4::SecurityHeadersMiddleware

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/middleware.rb

Overview

SecurityHeadersMiddleware -- injects security headers on every response. Config via env:

TINA4_FRAME_OPTIONS       — X-Frame-Options (default: SAMEORIGIN)
TINA4_HSTS                — Strict-Transport-Security max-age (default: "" = off)
TINA4_CSP                 — Content-Security-Policy (default: "default-src 'self'")
TINA4_REFERRER_POLICY     — Referrer-Policy (default: strict-origin-when-cross-origin)
TINA4_PERMISSIONS_POLICY  — Permissions-Policy (default: camera=(), microphone=(), geolocation=())

Class Method Summary collapse

Class Method Details

.attachObject

Register this middleware in the default chain (secure-by-default).

Unlike CSRF (opt-in via TINA4_CSRF) this is UNCONDITIONAL: a default app ships the security headers with no opt-in -- the SECHDR-DEC-01 posture that closes the SECHDR-OFF-BY-DEFAULT gap (the middleware existed with good defaults but was never registered). Idempotent (Middleware.use de-dupes). The framework calls it once at boot (Tina4.initialize!). Returns true.



787
788
789
790
# File 'lib/tina4/middleware.rb', line 787

def attach
  Tina4::Middleware.use(self)
  true
end

.before_security(request, response) ⇒ Object



792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
# File 'lib/tina4/middleware.rb', line 792

def before_security(request, response)
  response.headers["X-Frame-Options"] = ENV["TINA4_FRAME_OPTIONS"] || "SAMEORIGIN"
  response.headers["X-Content-Type-Options"] = "nosniff"

  # HSTS is HTTPS-only (SECHDR-DEC-02): a downgrade-protection header on a
  # plain-HTTP response is inert at best and ships a bad max-age on an
  # unencrypted scheme at worst. Emit it ONLY when TINA4_HSTS is set AND the
  # request is HTTPS -- Request.secure_scheme? honours x-forwarded-proto
  # (first hop) then rack.url_scheme, the same source of truth the session
  # cookie's Secure flag uses. Defensive env lookup keeps a non-Request from
  # turning every response into a 500 now that this runs on every request.
  hsts = ENV["TINA4_HSTS"] || ""
  env = request.respond_to?(:env) ? request.env : {}
  if !hsts.empty? && Tina4::Request.secure_scheme?(env)
    response.headers["Strict-Transport-Security"] = "max-age=#{hsts}; includeSubDomains"
  end

  warn_csp_default_once if ENV["TINA4_CSP"].nil?
  response.headers["Content-Security-Policy"] = ENV["TINA4_CSP"] || "default-src 'self'"
  response.headers["Referrer-Policy"] = ENV["TINA4_REFERRER_POLICY"] || "strict-origin-when-cross-origin"
  response.headers["X-XSS-Protection"] = "0"
  response.headers["Permissions-Policy"] = ENV["TINA4_PERMISSIONS_POLICY"] || "camera=(), microphone=(), geolocation=()"

  [request, response]
end

.warn_csp_default_onceObject

Warn once per process that the default CSP is in force (TINA4_CSP unset).

Secure-by-default keeps default-src 'self' (SECHDR-DEC-01), but that default is invisible: it blocks runtime-injected inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (a separate API or LiveKit host) -- and the failure surfaces only in the browser at runtime, long after a deploy has gone green. So the framework says so once, naming the escape hatch. It NEVER fails the boot or a request -- logging a heads-up must not be the reason the server or a request dies. Fires only when TINA4_CSP is ABSENT; setting it (even to empty) is an explicit opt-in.



828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
# File 'lib/tina4/middleware.rb', line 828

def warn_csp_default_once
  return if @csp_default_warned

  @csp_default_warned = true
  message = "TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy " \
    "\"default-src 'self'\" on every response. That default blocks runtime-injected " \
    "inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin " \
    "WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of " \
    "these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to " \
    "silence this notice without changing behaviour, set TINA4_CSP=\"default-src 'self'\"."
  begin
    Tina4::Log.warning(message)
  rescue StandardError
    # Logging must never break a request.
    warn(message)
  end
end