PROXY Protocol v1 パーサーのセキュリティ修正とバグ修正

puma/puma

Puma 7.2.1 のバックポートとして、PROXY Protocol v1 パーサーに存在した複数の脆弱性とバグが修正されました。最大行長のバリデーション追加、非プロキシリクエストの誤検知修正、そしてキープアライブ接続での二重適用防止が含まれます。

背景

Puma の PROXY Protocol v1 サポートには、実運用上問題となる複数の欠陥が存在していました。具体的には、CRLF を含まない過長なプロキシヘッダーの検出漏れ、HTTP ボディ内に埋め込まれた PROXY 行への誤反応、そしてキープアライブ接続における2リクエスト目以降へのプロトコル再適用という3点が問題でした。

PROXY Protocol v1 の仕様では、ヘッダー行の最大長は PROXY プレフィックスを含めて 107バイト と定められています。しかし従来の実装ではこの上限チェックが不完全で、CRLF が現れる前に最大長に達した場合のエラー処理が欠落していました。また、^ アンカーを使った正規表現がバッファ内の任意行にマッチする可能性も潜在していました。

技術的な変更

最大行長バリデーションの追加

lib/puma/const.rbPROXY_PROTOCOL_V1_MAX_LENGTH = 107 が定数として追加されました。あわせて PROXY_PROTOCOL_V1_REGEX の行頭アンカーが ^ から \A に変更され、バッファ先頭以外の行へのマッチが防止されています。

変更前:

PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze

変更後:

PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
PROXY_PROTOCOL_V1_MAX_LENGTH = 107

try_to_parse_proxy_protocol の全面的な書き直し

lib/puma/client.rbtry_to_parse_proxy_protocol メソッドが全面的に書き直されました。変更のポイントは以下の3点です。

  • CRLF 未到達時の部分バッファ判定: @buffer.include? "\r\n" から @buffer.index "\r\n" を使った判定に変更し、CRLF が未到達の場合に "PROXY " で始まる可能性があるか ("PROXY ".start_with? @buffer) を確認してから待機するか接続を切断するかを判断します。
  • 最大長超過時の即時エラー: PROXY プレフィックスで始まるバッファが CRLF なしに PROXY_PROTOCOL_V1_MAX_LENGTH バイトに達した場合、ConnectionErrorraise して接続を拒否します。
  • 非プロキシリクエストの正確なスキップ: CRLF は存在するが PROXY ヘッダーにマッチしない場合(通常の HTTP リクエスト)は、@read_proxy = false として以降のパース試行をスキップします。

変更後の主要ロジック:

crlf_index = @buffer.index "\r\n"

unless crlf_index
  if "PROXY ".start_with? @buffer
    return false
  elsif @buffer.start_with? "PROXY "
    if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
      raise ConnectionError, "PROXY protocol v1 line is too long"
    end
    return false
  end

  @read_proxy = false
  return true
end

if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
  raise ConnectionError, "PROXY protocol v1 line is too long"
end

キープアライブ接続での二重適用防止

reset メソッドで @read_proxy を初期化する際の条件が変更されました。

変更前:

@read_proxy = !!@expect_proxy_proto

変更後:

@read_proxy = !!@expect_proxy_proto && @requests_served.zero?

これにより、同一 TCP 接続上の2リクエスト目以降では @read_proxyfalse に設定され、PROXY ヘッダーのパースが試みられなくなります。PROXY Protocol v1 はコネクション確立時に1度だけ送信されるため、この変更は仕様に沿った正しい動作です。

設計判断

部分バッファへの対応として、"PROXY ".start_with? @buffer という逆方向の start_with? チェックが採用されています。これは「バッファがまだ途中まで届いている状態で、将来的に "PROXY " になる可能性がある」を検出するための判定です。たとえばバッファが "PRO" の段階では待機を続け、"HTTP" であれば即座にプロキシ解析をスキップするという、ストリーム処理として自然な設計といえます。

\A アンカーへの変更は、HTTP レスポンスボディ中に PROXY TCP4 ...\r\n が埋め込まれた場合に正規表現が誤マッチするリスクを排除しています。キープアライブ修正と合わせて、PROXY ヘッダーがコネクション先頭の1行目にのみ現れることを複数の層で保証する多層防御の設計になっています。

まとめ

本 PR は、PROXY Protocol v1 パーサーに存在していた最大長チェックの不備、正規表現アンカーの誤り、キープアライブ接続での二重パースという3つの問題を修正しています。いずれも仕様の厳密な実装への修正であり、不正なバイト列による意図しない動作を防ぐ堅牢性の向上といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
c765ef18

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ OK

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

カスタムMarkdown構文 ✓ OK

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

対象読者への適合性 ✓ OK

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

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

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

Diff内容との照合 ✓ OK

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

技術用語の正確性 ✓ OK

技術用語の正確な使用

説明の技術的正確性 ✓ OK

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

事実の突合 ✓ OK

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

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

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

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

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

外部知識の正確性 ✓ OK

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

時間表現の正確性 ✓ OK

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