DiffDaily

Deep & Concise - OSS変更の定点観測

[Rails] HTTPローカル環境でCSRFヘッダーオンリー保護を利用可能に

rails/rails

背景: セキュアコンテキストの制約とローカル環境の問題

RailsのCSRF保護では、Sec-Fetch-Siteヘッダーを検証するヘッダーオンリーアプローチが提供されています。しかし、HTTPSを使用しないローカルネットワーク環境では、ブラウザがセキュアコンテキストとして認識しないため、Sec-Fetch-Siteヘッダーが送信されません。この結果、非GETリクエストがCSRF保護により拒否され、ローカル環境でのアプリケーション利用に支障をきたしていました。

この問題はFizzyプロジェクトのディスカッションで報告され、PR #2291で解決されました。本PRはその変更をRails本体にアップストリームしたものです。

技術的変更内容

CSRF検証ロジックの拡張

verified_via_header_only?メソッドに新たな条件分岐が追加されました。

変更前:

def verified_via_header_only?
  SAFE_FETCH_SITES.include?(sec_fetch_site_value) ||
    (sec_fetch_site_value == "cross-site" && origin_trusted?)
end

変更後:

def verified_via_header_only?
  SAFE_FETCH_SITES.include?(sec_fetch_site_value) ||
    (sec_fetch_site_value == "cross-site" && origin_trusted?) ||
    (sec_fetch_site_value.nil? && !request.ssl? && !ActionDispatch::Http::URL.secure_protocol)
end

新たに追加された条件は以下の3つを同時に満たす場合にリクエストを許可します:

  1. sec_fetch_site_value.nil? - Sec-Fetch-Siteヘッダーが存在しない
  2. !request.ssl? - HTTPリクエストである
  3. !ActionDispatch::Http::URL.secure_protocol - アプリケーションでSSL強制が無効

重要な点として、Originヘッダーの検証は既存のorigin_trusted?チェックとは独立して常に実行されます。

テストケースの追加

ローカルHTTP環境での動作を保証するため、複数のテストケースが追加されました。

test "allows POST with missing Sec-Fetch-Site header on HTTP when force_ssl is disabled" do
  with_secure_protocol(false) do
    post :index
    assert_response :success
  end
end

test "blocks POST without Sec-Fetch-Site header when request is HTTPS" do
  @request.set_header "HTTPS", "on"
  assert_raises(ActionController::InvalidCrossOriginRequest) do
    post :index
  end
end

test "blocks POST without Sec-Fetch-Site header when request is HTTP but force_ssl is enabled" do
  with_secure_protocol(true) do
    assert_raises(ActionController::InvalidCrossOriginRequest) do
      post :index
    end
  end
end

これらのテストにより、以下のシナリオが検証されます:

  • HTTP + force_ssl無効: リクエスト許可
  • HTTPS環境: リクエスト拒否(既存の安全な動作を維持)
  • HTTP + force_ssl有効: リクエスト拒否(本番環境相当の設定では厳格に検証)

既存テストの調整

セッション・Cookie関連のミドルウェアテストでは、CSRF検証を明示的に失敗させるためSec-Fetch-Site: cross-siteヘッダーが追加されました。

header "Sec-Fetch-Site", "cross-site"

get "/foo/write_session"
get "/foo/read_session"
assert_equal "1", last_response.body

post "/foo/read_session"  # Read session using POST request failing CSRF check
assert_equal "nil", last_response.body

この変更により、テストの意図がより明確になり、ヘッダー不在時の新しい許可ロジックの影響を受けなくなりました。

セキュリティ上の考慮事項

この変更は、以下の理由により既存のセキュリティレベルを維持しています:

  1. 限定的な適用範囲: HTTPかつforce_ssl無効という、明確にローカル環境を想定した条件下でのみ動作
  2. Origin検証の継続: Sec-Fetch-Siteヘッダーの有無に関わらず、Originヘッダーの検証は常に実行される
  3. 本番環境への影響なし: HTTPS環境やforce_sslが有効な本番環境では従来通りの厳格な検証が継続

影響を受けるユースケース

以下のような環境でヘッダーオンリーCSRF保護が利用可能になります:

  • 社内ネットワークでHTTPを使用するアプリケーション
  • 開発環境でHTTPSを使用しない場合(config.force_ssl = false
  • HTTPでアクセスされるイントラネットアプリケーション

これらの環境でも、クロスサイトリクエストからの保護はOriginヘッダー検証により維持されます。