Rails
Basic認証
タイミング攻撃

Rails で、複数 ID に対して OR 条件で BASIC 認証をかける方法

前提

  • Rails の話
  • 下記のような2組の ID/PW ペアが存在するとする
    • user1 / pass1
    • user2 / pass2

やりたいこと

  • あるアクションに対して、 user1 または user2 だけがアクセスできるような BASIC 認証をかけたい

結論

以下のように実装すればよい。

class FooController < ApplicationController
  before_action :multiple_authenticate

  def index
  end

  protected

  def multiple_authenticate
    authenticate_or_request_with_http_basic do |username, password|
      (
        ActiveSupport::SecurityUtils.secure_compare(username, 'user1') &
        ActiveSupport::SecurityUtils.secure_compare(password, 'pass1')
      ) | (
        ActiveSupport::SecurityUtils.secure_compare(username, 'user2') &
        ActiveSupport::SecurityUtils.secure_compare(password, 'pass2')
      )
    end
  end
end

もちろん、ID/PW をハードコードしているところは適宜セキュアにすること(環境変数を使う、secret.yml を使うなど)。

解説

まず、「やりたいこと」で示した「OR 条件による BASIC 認証」を実現する標準機能はどうやら存在しないようでした。
仕方ないので自前で実装するわけですが、ここで普通に BASIC 認証するときに使う http_basic_authenticate_with の実装を参考にします。ソースはこちら (https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/http_authentication.rb#L71-L81) 。

このメソッドは、直下で定義されている authenticate_or_request_with_http_basic というメソッド(名前が似ていてややこしい)を使って before_action を設定しているだけのようです。authenticate_or_request_with_http_basic は、

  • リクエストヘッダに BASIC 認証情報が含まれていなければ 401 を返す(ID / PW を聞く)
  • リクエストヘッダに BASIC 認証情報が含まれていればブロックを評価し、
    • truthy なら処理を続行
    • falsy なら 401 を返す(ID / PW を聞く)

という挙動をします。ということは、authenticate_or_request_with_http_basic のブロック内で user1 または user2 の ID/PW と一致するかどうかをチェックすればよい、ということになります。

ここで、http_basic_authenticate_with のソースに目を戻します(繰り返しになりますが、メソッド名がややこしいので混乱しないよう注意してください)。この実装を見ると、ID/PW の比較に変わったメソッドを使っています。

# This comparison uses & so that it doesn't short circuit and
# uses `secure_compare` so that length information
# isn't leaked.
ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
ActiveSupport::SecurityUtils.secure_compare(password, options[:password])

ここでは、セキュリティのために以下の2つの工夫がこらされています。

  • 文字列の比較に secure_compare を使っている
    • コメントにも書かれていますが、これによって比較対象の文字長がバレないようになっています。
    • 文字列比較が「そもそも文字長が一致してなければ即エラーを返す」という実装になっている場合、比較処理がすぐ終わるかどうかで文字長を推理できてしまいます。いわゆるタイミング攻撃というものです。
    • 実装を見てみると、与えられた文字列を SHA256 でハッシュ化してから比較しています。つまり常に 256 文字同士の比較になるため、セキュアというわけです。
    • 参考: https://github.com/rails/rails/commit/fa487763d98ccf9c3e66fdb44f09af5c37a50fe5
  • AND 演算に & を使っている
    • コメントにも書かれていますが、これによって短絡評価がされないようになっています。
    • 短絡評価されてしまうと、name が正しいときと間違っているときとで処理時間に差が出てしまい、ここでもタイミング攻撃のリスクが発生します。
    • 参考: https://github.com/rails/rails/commit/17e6f1507b7f2c2a883c180f4f9548445d6dfbda

これに倣うと、今回実装する複数 ID/PW ペアに対する OR 条件での認証も、

  • 文字列の比較に secure_compare を使う
  • OR 条件に | を使う

という方針で実装すればよさそうです。その実装例が上記で示したものとなっています。