Mailgun Action Mailboxで不正なタイムスタンプを適切に拒否する
Mailgun Action Mailboxのイングレスにおいて、不正な形式のタイムスタンプが送信された場合にArgumentErrorでサーバーエラーが発生していた問題が修正されました。これにより、不正リクエストは意図通り401 Unauthorizedとして拒否されるようになります。
背景
Authenticatorクラスの初期化処理に、不正なタイムスタンプがArgumentErrorを引き起こすというセキュリティ上の問題がありました。#57315として報告されたこの問題では、認証チェックが完了する前に例外が発生するため、リクエストが401 Unauthorizedではなく500系のサーバーエラーとして処理されてしまっていました。
具体的には、timestampとして"not-a-number"のような文字列を渡すと、Integer(timestamp)がArgumentErrorを送出します。本来であれば認証失敗として扱うべきリクエストが、エラーハンドリングの経路に乗らずにイングレスをエスケープできる状態でした。HMAC署名の検証やタイムスタンプの鮮度チェックに到達する前に例外が発生するため、認証ロジック全体をバイパスしてしまうことが問題の本質です。
技術的な変更
Authenticator#initializeとプライベートメソッドrecent?の2箇所が修正され、タイムスタンプのパースと利用が分離されました。
変更前:
def initialize(key:, timestamp:, token:, signature:)
@key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
end
def recent?
Time.at(timestamp) >= 2.minutes.ago
end
変更後:
def initialize(key:, timestamp:, token:, signature:)
@key, @timestamp, @token, @signature = key, timestamp.to_s, token, signature
@parsed_timestamp = Integer(timestamp, exception: false)
end
def recent?
@parsed_timestamp && Time.at(@parsed_timestamp) >= 2.minutes.ago
end
変更のポイントは2点あります。第一に、@timestampにはHMAC署名の入力として使用する生の文字列を保持するようになりました。expected_signatureメソッドはtimestampを文字列としてHMACの入力に連結するため、この変更は署名検証の動作に影響しません。第二に、Integer(timestamp, exception: false)を使って@parsed_timestampを別途パースします。exception: falseオプションにより、パースに失敗した場合は例外の代わりにnilが返ります。
recent?では@parsed_timestampがnilでないことを事前にチェックするため、不正なタイムスタンプの場合はfalseを返し、認証失敗として扱われます。あわせてテストも追加され、不正なタイムスタンプを持つリクエストが401 Unauthorizedを返すことが検証されています。
設計判断
HMAC署名の入力とタイムスタンプのパースを分離する方式が採用されました。
MailgunのHMAC署名は"#{timestamp}#{token}"という文字列を入力として生成されます。署名検証の正確性を保つためには、受け取ったtimestampの生の文字列をそのままHMAC入力に使う必要があります。もしInteger変換後の値を使うと、"1234"と" 1234"のような入力が同一視され、署名の検証が緩くなる可能性があります。@timestampを文字列のまま保持することで、この問題を回避しています。
また、Integer(timestamp, exception: false)はInteger(timestamp)をrescue ArgumentErrorで囲む代替手段と比べて、例外処理のオーバーヘッドなしに失敗をnilで表現できる明快なアプローチです。nilのfalsy性を@parsed_timestamp &&で活用するrecent?の実装も、追加の条件分岐を最小限に抑えた設計といえます。
まとめ
この修正は、タイムスタンプのパース失敗を例外ではなくnilで表現することで、認証フローを一貫してfalseで完結させる変更です。HMAC入力用の生文字列とパース済み整数を分離した設計により、署名検証の正確性を維持しながら不正入力への堅牢性を高めています。