PROXY Protocol v1 パーサーのセキュリティ修正とバグ修正
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.rb に PROXY_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.rb の try_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バイトに達した場合、ConnectionErrorをraiseして接続を拒否します。 -
非プロキシリクエストの正確なスキップ: 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_proxy が false に設定され、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つの問題を修正しています。いずれも仕様の厳密な実装への修正であり、不正なバイト列による意図しない動作を防ぐ堅牢性の向上といえます。