アンダースコアヘッダーのハードニングオプションを追加
Pumaにallow_underscore_headers設定が追加され、アンダースコアを含むリクエストヘッダーを破棄してヘッダーインジェクション攻撃を防ぐ手段が提供されました。デフォルトは互換性のためtrueですが、falseへの移行が推奨されています。
背景
Rackのenv正規化により、ハイフン区切りとアンダースコア区切りのヘッダー名が区別できなくなるという問題があります。具体的には、X-Forwarded-HostとX_Forwarded_HostはいずれもHTTP_X_FORWARDED_HOSTに正規化されます。多くのRackアプリケーションはX-Forwarded-ForやX-Forwarded-HostなどのX-Forwarded-*ヘッダーを上流プロキシやCDNが設定したものとして信頼しているため、この挙動はセキュリティリスクになります。
Pumaはすでに「衝突ケース」(ハイフン版とアンダースコア版の両方が届く場合)を保護していました。ハイフン版を優先し、アンダースコア版で上書きされないよう内部で管理しています。しかし「単独アンダースコアケース」は未対処でした。上流プロキシが正規のX-Forwarded-*ヘッダーを付与しないリクエストパスが存在したり、クライアントが送信したアンダースコア版ヘッダーを除去しなかったりした場合、Pumaはクライアントが制御するヘッダーを信頼済みのRack envキーへ正規化してしまいます。アプリからは「上流がenv["HTTP_X_FORWARDED_HOST"]をevil.exampleに設定した」ように見えますが、実際には上流は正規ヘッダーを一切設定していません。
このPRはnginxのアンダースコアヘッダー処理に類似した仕組みを導入し、デプロイメントがより安全な挙動をオプトインできるようにします。
技術的な変更
変更の中心はlib/puma/client_env.rbのreq_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.rbのDEFAULTSにallow_underscore_headers: trueを追加 -
lib/puma/dsl.rbにallow_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_disallowedとtest_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による監査機能を組み合わせることで、既存アプリケーションの互換性を維持しながら段階的にセキュリティを強化できる仕組みが整いました。