From 84ed462f4033d26fc872822ae258cebe5b0229ad Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 11 Jun 2026 08:04:42 +1000 Subject: [PATCH] Require CLINCH_HOST in deployed environments; drop request-host fallback determine_base_url fell back to request.host when CLINCH_HOST was unset. Rails resolves request.host from X-Forwarded-Host behind a trusted proxy, so a spoofed header could make the forward-auth login redirect point at an attacker origin (host-header phishing). - Add config/initializers/clinch_host.rb: fail fast at boot in any non-local environment when CLINCH_HOST is blank. It anchors the OIDC issuer, WebAuthn RP ID, and login redirect, so it must be explicit, never inferred. - determine_base_url now uses CLINCH_HOST (guaranteed in production) with a safe localhost default for dev/test, and never reads the request host. - Simplify the spoofed-host regression test now that the fallback is safe. Verified: production boot aborts with a clear message when CLINCH_HOST is blank, and boots normally when set. Co-Authored-By: Claude Fable 5 --- .../api/forward_auth_controller.rb | 19 +++++++------------ config/initializers/clinch_host.rb | 13 +++++++++++++ .../forward_auth_integration_test.rb | 11 +++-------- 3 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 config/initializers/clinch_host.rb diff --git a/app/controllers/api/forward_auth_controller.rb b/app/controllers/api/forward_auth_controller.rb index e5f218c..39b678a 100644 --- a/app/controllers/api/forward_auth_controller.rb +++ b/app/controllers/api/forward_auth_controller.rb @@ -243,18 +243,13 @@ module Api def determine_base_url(redirect_url) return redirect_url if redirect_url.present? - if ENV["CLINCH_HOST"].present? - host = ENV["CLINCH_HOST"] - host.match?(/^https?:\/\//) ? host : "https://#{host}" - else - request_host = request.host || request.headers["X-Forwarded-Host"] - if request_host.present? - Rails.logger.warn "ForwardAuth: CLINCH_HOST not set, using request host: #{request_host}" - "https://#{request_host}" - else - raise StandardError, "ForwardAuth: CLINCH_HOST environment variable not set and no request host available." - end - end + # CLINCH_HOST is the IdP's canonical origin and is mandatory in deployed + # environments (enforced at boot in config/initializers/clinch_host.rb). + # We never fall back to the request host: a spoofed X-Forwarded-Host would + # otherwise redirect the login flow to an attacker-controlled origin. The + # localhost default only applies to local dev/test. + host = ENV["CLINCH_HOST"].presence || "http://localhost:3000" + host.match?(%r{\Ahttps?://}) ? host : "https://#{host}" end end end diff --git a/config/initializers/clinch_host.rb b/config/initializers/clinch_host.rb new file mode 100644 index 0000000..d29ed84 --- /dev/null +++ b/config/initializers/clinch_host.rb @@ -0,0 +1,13 @@ +# CLINCH_HOST is this IdP's canonical external origin, e.g. https://auth.example.com. +# It anchors the OIDC issuer, the WebAuthn RP ID, and the forward-auth login +# redirect. In deployed (non-local) environments it MUST be set explicitly and +# never inferred from request headers — X-Forwarded-Host is attacker-influenceable, +# so inferring the origin from it would allow host-header phishing and open +# redirects. Fail fast at boot rather than start in an unsafe configuration. +unless Rails.env.local? + if ENV["CLINCH_HOST"].blank? + raise "CLINCH_HOST must be set (e.g. https://auth.example.com). It is the " \ + "canonical origin of this Clinch instance and must not be inferred " \ + "from request headers." + end +end diff --git a/test/integration/forward_auth_integration_test.rb b/test/integration/forward_auth_integration_test.rb index e23aa9b..8eeeff2 100644 --- a/test/integration/forward_auth_integration_test.rb +++ b/test/integration/forward_auth_integration_test.rb @@ -177,13 +177,10 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest end test "spoofed X-Forwarded-Host is not reflected as a redirect target" do - # CLINCH_HOST pins the IdP origin (as in production) so base_url cannot be - # influenced by the request; this isolates the return_to/redirect behaviour. - original_clinch_host = ENV["CLINCH_HOST"] - ENV["CLINCH_HOST"] = "https://auth.example.com" - # No forward-auth app exists for evil.com, and no valid rd is supplied. The - # attacker-controlled host must NOT be stored or reflected into the signin URL. + # attacker-controlled host must NOT be stored or reflected into the signin URL, + # and base_url must come from CLINCH_HOST (or the safe localhost default in + # test) rather than the request host. get "/api/verify", headers: { "X-Forwarded-Host" => "evil.com", "X-Forwarded-Uri" => "/steal" @@ -193,8 +190,6 @@ class ForwardAuthIntegrationTest < ActionDispatch::IntegrationTest assert_match %r{/signin}, response.location refute_includes response.location, "evil.com" refute_match(/evil\.com/, session[:return_to_after_authenticating].to_s) - ensure - ENV["CLINCH_HOST"] = original_clinch_host end test "return URL functionality after authentication" do