x-runtime は消すべきなのか

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

- [Nginxでレスポンスヘッダの一部を隠蔽する方法 - Qiita](https://qiita.com/noraworld/items/50781bcaa5bf28802cd0)
- [Nginxセキュリティ設定 - Qiita](https://qiita.com/hideji2/items/1421f9bff2a97a5e5794)

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

参照した記事に説明がないので推測でしかありませんが、 x-runtime を使って Timing Attack が可能になることを根拠にしているように思います。 Timing Attack とは[サイドチャネル攻撃](https://ja.wikipedia.org/wiki/%E3%82%B5%E3%82%A4%E3%83%89%E3%83%81%E3%83%A3%E3%83%8D%E3%83%AB%E6%94%BB%E6%92%83)の一種で、 x-runtime の値をヒントにしてサーバ内のセキュアな情報を盗むことができる可能性があります。

### なぜヒントになるのか

```rb
# @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`](https://github.com/rack/rack/blob/6b942ff54/lib/rack/utils.rb#L366-L380) があります。

```rb
# 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` のままかどうかを返しています。そして、

```rb
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 を計算します。([コード](https://github.com/rack/rack/blob/6b942ff/lib/rack/session/cookie.rb#L177-L182)）

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

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

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

```rb
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)` なアルゴリズムで比較する必要があります。

## 参考

- [Timing Attacks · TLSeminar](https://tlseminar.github.io/timing-attacks/)