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
a
と b
をバイト毎に比較し、その結果を r
に格納し、最後に r
が 0
のままかどうかを返しています。そして、
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)
なアルゴリズムで比較する必要があります。