アンダースコアヘッダーのハードニングオプションを追加

puma/puma

Pumaにallow_underscore_headers設定が追加され、アンダースコアを含むリクエストヘッダーを破棄してヘッダーインジェクション攻撃を防ぐ手段が提供されました。デフォルトは互換性のためtrueですが、falseへの移行が推奨されています。

背景

Rackのenv正規化により、ハイフン区切りとアンダースコア区切りのヘッダー名が区別できなくなるという問題があります。具体的には、X-Forwarded-HostX_Forwarded_HostはいずれもHTTP_X_FORWARDED_HOSTに正規化されます。多くのRackアプリケーションはX-Forwarded-ForX-Forwarded-HostなどのX-Forwarded-*ヘッダーを上流プロキシやCDNが設定したものとして信頼しているため、この挙動はセキュリティリスクになります。

Pumaはすでに「衝突ケース」(ハイフン版とアンダースコア版の両方が届く場合)を保護していました。ハイフン版を優先し、アンダースコア版で上書きされないよう内部で管理しています。しかし「単独アンダースコアケース」は未対処でした。上流プロキシが正規のX-Forwarded-*ヘッダーを付与しないリクエストパスが存在したり、クライアントが送信したアンダースコア版ヘッダーを除去しなかったりした場合、Pumaはクライアントが制御するヘッダーを信頼済みのRack envキーへ正規化してしまいます。アプリからは「上流がenv["HTTP_X_FORWARDED_HOST"]evil.exampleに設定した」ように見えますが、実際には上流は正規ヘッダーを一切設定していません。

このPRはnginxのアンダースコアヘッダー処理に類似した仕組みを導入し、デプロイメントがより安全な挙動をオプトインできるようにします。

技術的な変更

変更の中心はlib/puma/client_env.rbreq_env_post_parseメソッドの拡張です。アンダースコアヘッダーの検出・記録・破棄ロジックが追加されました。

変更前req_env_post_parseは、,(内部でアンダースコアを表す文字)を含むHTTPヘッダーを見つけた際、UNMASKABLE_HEADERSに該当しなければ正規化されたキーへの追加を試みていました。

変更後は以下の処理フローになります:

@env.each do |k,v|
  next unless k.start_with?("HTTP_") && k.include?(",")

  (underscore_headers ||= []) << k.delete_prefix("HTTP_").tr("_,", "-_")
  next if @allow_underscore_headers && UNMASKABLE_HEADERS.key?(k)

  (to_delete ||= []) << k
  next unless @allow_underscore_headers

  new_k = k.tr(",", "_")
  next if @env.key?(new_k)

  (to_add ||= {})[new_k] = v
end

アンダースコア起源のヘッダーが存在する場合、allow_underscore_headersの値にかかわらず常にそのヘッダー名(値は含まない)がunderscore_headers配列に収集されます。allow_underscore_headers falseの場合はヘッダーを削除するのみで正規化キーへの追加は行いません。

設定の伝播経路は以下のとおりです:

  • lib/puma/configuration.rbDEFAULTSallow_underscore_headers: trueを追加
  • lib/puma/dsl.rballow_underscore_headersメソッドを追加(設定ファイルから呼び出せるDSL)
  • lib/puma/server.rb@optionsから値を読み取り、new_client時にClientインスタンスへ設定
  • lib/puma/client.rb@allow_underscore_headersインスタンス変数とattr_writerを追加

lib/puma/const.rbにはPUMA_UNDERSCORE_HEADERS = "puma.underscore_headers"定数が追加され、Rack envへの書き込みキーが定義されています。

テストではtest_underscore_single_disallowedtest_underscore_collision_disallowedの2ケースが追加されました。前者はアンダースコアヘッダー単独の場合にHTTP_X_FORWARDED_FORが環境変数に現れないことを確認し、後者は衝突ケースで正規ヘッダー(ハイフン版)が保護されることを検証しています。いずれのケースでもenv[PUMA_UNDERSCORE_HEADERS]にヘッダー名が記録されることを確認しています。

設計判断

デフォルト値をtrue(現状維持)とし、falseを推奨設定として位置づける段階的移行戦略が採用されています。

puma.underscore_headers envキーはallow_underscore_headersの設定値にかかわらず常に記録される設計です。これにより、設定を変更する前にアンダースコアヘッダーを含むトラフィックの実態を監査できます。PRでは「この値はクライアントがトリガーできるため、外部レポートはサンプリングまたはレート制限すること」と明記されており、可観測性とセキュリティのバランスが考慮されています。

ヘッダー名のみを記録し値は記録しない点も意図的な設計です。ヘッダー値はユーザー制御データであり、ログや監視システムへの漏洩リスクを避けるためです。

サーバー起動オプションにallow_underscore_headersキーが存在しない場合に備え、@options.fetch(:allow_underscore_headers, true)でフォールバックを設定しています。テストtest_allow_underscore_headers_defaults_to_true_when_option_is_absentがこの境界条件を明示的に検証しています。

まとめ

本PRはRackのヘッダー正規化に起因するヘッダーインジェクションリスクに対し、nginxと同様のアプローチで対処するものです。allow_underscore_headers falseの設定とpuma.underscore_headersによる監査機能を組み合わせることで、既存アプリケーションの互換性を維持しながら段階的にセキュリティを強化できる仕組みが整いました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
0858d3bb

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という3部構成が明確に適用されており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:lib/puma/client_env.rb)とGitHubリンク記法([PR #3937](URL))が、ガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Rackのヘッダー正規化やヘッダーインジェクションといった専門的なトピックを扱っており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成で、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されており、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている`lib/puma/client_env.rb`のコードは、提供されたDiffの変更内容と正確に一致しています。他のファイルへの言及もDiff内容と整合しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Rack env正規化」「衝突ケース」「単独アンダースコアケース」など、PRの文脈で使われる技術用語を正確に用いて解説できています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`allow_underscore_headers`の設定値による挙動の違いや、`puma.underscore_headers`の監査機能に関する説明が、コードのロジックと一致しており技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのTitle、Description、Diff内容で裏付けられており、ハルシネーション(創作された情報)は検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#3937)、設定名(allow_underscore_headers)、定数名(PUMA_UNDERSCORE_HEADERS)などの固有名詞がすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「アンダースコアヘッダーのハードニングオプションを追加」は、PRのタイトル「Add underscore header hardening option」を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRに記載のないLTSやリリース日程などの外部知識は含まれておらず、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

PR Descriptionにある「Puma already protects...(Pumaはすでに保護している)」といった時間表現を、記事内で「Pumaはすでに...保護していました」と正確に反映できています。