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 <noreply@anthropic.com>
This commit is contained in:
Dan Milne
2026-06-11 08:04:42 +10:00
parent 96a657e349
commit 84ed462f40
3 changed files with 23 additions and 20 deletions

View File

@@ -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