x-runtime は消すべきなのか

  • 9
    いいね
  • 0
    コメント

Rails などの WAF の中にはサーバでの処理時間を x-runtime ヘッダとしてクライアントに返すものがありますが、セキュリティ上の観点から x-runtime ヘッダを消すことを奨励していることも多いです。それはなぜでしょうか。

Timing Attack

なぜ x-runtime を消すのか。逆にいえば x-runtime を残すとどういうセキュリティ上のリスクがあるのか。

参照した記事に説明がないので推測でしかありませんが、 x-runtime を使って Timing Attack が可能になることを根拠にしているように思います。 Timing Attack とはサイドチャネル攻撃の一種で、 x-runtime の値をヒントにしてサーバ内のセキュアな情報を盗むことができる可能性があります。

なぜヒントになるのか

# @param user_input [String]
# @param secret_key [String]
# @return [Boolean]
def compare(user_input, secret_key)
  return false if user_input.size != secret_key.size
  (0...user_input.size).each do |i|
    return false if user_input[i] != secret_key[i]
  end
  true
end

ユーザの入力が正しいかどうかチェックするのにこんな関数を使っていたとします。以下のように x-runtime の値をみながら入力を変えていくことで secret_key を推測することが可能です。

  • 入力の長さが違うと x-runtime が小さくなるので、その長さを変更しながら試すことで secret_key の長さを推定することができる
  • secret_key"hello" だったとすると、入力が "catch""hella" では return するまでにかかるループの回数が変わるので、 secret_key を推測することができる

Timing Attack を防ぐ方法

生のデータをそのまま比較するのではなく、HMAC などハッシュ値を使えばもともとの入力内容によらず user_input の長さは一定になります。

また一致しないバイト値をみつけたら即座に false を返すのではなく、一通りバイト列を比較し終えてから結果を返せば compare 関数が O(n) だったのを O(1) にすることができるので、 Timing Attack を防ぐことができます。

実装例

Rack にはそのようなセキュアな比較を行うための Rack::Utils.secure_compare があります。

# Constant time string comparison.
#
# NOTE: the values compared should be of fixed length, such as strings
# that have already been processed by HMAC. This should not be used
# on variable length plaintext strings because it could leak length info
# via timing attacks.
def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  l = a.unpack("C*")

  r, i = 0, -1
  b.each_byte { |v| r |= v ^ l[i+=1] }
  r == 0
end

ab をバイト毎に比較し、その結果を r に格納し、最後に r0 のままかどうかを返しています。そして、

def digest_match?(data, digest)
  return unless data && digest
  @secrets.any? do |secret|
    Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
  end
end

Rack::Utils.secure_compare を呼ぶ側では HMAC を計算します。(コード

x-runtime を隠すべきか

Timing Attack の観点から言えば x-runtime が返す値の中にセキュアな情報の比較にかかった時間が含まれていて、かつその比較アルゴリズムが情報によって変化する(O(n))なら隠す必要があります。しかし、 Rack::Utils.secure_compare のような実装を使っているなら必ずしもその必要はありません。

Rails の場合

Rails の場合 x-runtime ヘッダは Rack::Runtime ミドルウェアが作っています。つまり Rails のアプリの処理時間を表しており、その過程でセキュアな情報の比較を O(n) なアルゴリズムで行っていると問題になります。

たとえばログイン処理が以下のように String#== を使っていると Timing Attack が可能になります。

class User < ApplicationRecord
  # @param password [String]
  # @return [Boolean]
  def authenticate(password)
    password_digest == generate_hmac(password + salt)
    # こうすべき
    # Rack::Utils.secure_compare(password_digest, generate_hmac(password + salt)
  end
end

まとめ

心配なら x-runtime は消していいが、消さなかったとしても攻撃できないよう対処することは可能です。

x-runtime が無くても攻撃者は全体のレスポンスタイムから Timing Attack をしかけることは可能なので、 x-runtime を消したら安全というものではなく、返していようがいまいがセキュアな情報は O(1) なアルゴリズムで比較する必要があります。

参考