Railsアプリの認証システムをセキュアにする4つの方法(翻訳)

こんにちは、hachi8833です。
TechRachoでよく記事を翻訳させていただいているDuck Type Labの記事をお送りいたします。DeviseやRodauthといった認証システムを扱っていますが、広く認証システム一般について役に立つと思います。

概要

Railsアプリの認証システムをセキュアにする4つの方法(翻訳)

Ruby on Railsの認証フレームワークはある意味で議論になりやすいトピックです。たとえばDeviseはその中でも人気の高いオプションです。Deviseは、明快なドキュメントがないために理解が難しく、カスタマイズも困難で、他のgemか自前の認証システムを検討するほうがまだましかもという気にさせられるという点がよく批判されます(たぶんそのとおりかもしれません)。Deviseの支持者は、Deviseが長年に渡ってベテラン開発者の酷使に耐えてきた実績があるという点と、認証システムを自作すると十分に監査されたソフトウェア部品によって得られるセキュリティの多くを諦めなければならなくなるという点を指摘しています。

こうした議論をご存知でない方は、次の点について考えてみていただければと思います。RailsチュートリアルやRailsCastなどで解説されているように#has_secure_passwordを使い、SecureRandomで生成したパスワードリセットトークンを十分短い期間で期限切れにするだけでよさそうなものです。にもかかわらず、なぜDeviseが必要なのかという点です。

  • RailsチュートリアルやRailsCastで説明されている認証システムには、何か避けようのないセキュリティ問題があるのでしょうか。
  • ユーザー認証が必要になったら、どんなときでもDeviseのようなgemを使うべきなのでしょうか。
  • Deviseなどのgemを導入すればアプリの認証は永久にセキュアになるのでしょうか。

その答えは、他の多くの分野のソフトウェアと同様「状況次第」ということになります。認証は、アプリやインフラから独立して存在するものではないからです。アプリの認証システムが十分セキュアであっても、アプリの他の部分に弱点があればユーザーが危険にさらされるかもしれません。逆に、アプリの他の部分やインフラのセキュリティがきちんと作られているのであれば、セキュリティがトップクラスでない認証システムを導入することも不可能ではありません。

アプリ開発者がこの問いに十分な回答を示すのに必要なのはただひとつ、セキュリティと認証について一般的な理解を深めることだけです。そうすることで、アプリの開発やセキュリティ強化で必ず発生する「トレードオフ」を正しくこなせるようになります。

以下で説明する4つの方法は、DeviseやClearanceなどのサードパーティgemを使う場合や独自の認証システムを構築する場合にかかわらず、アプリの認証システムのセキュリティを高めるのに役に立ちます。効能は人によって異なりますが、少なくとも4番目の方法で皆さんが何らかの重要な知見を得られることを願っています。

1. リクエストを絞り込む

攻撃者がアプリのユーザーを攻略するうえで最も簡単なのは、スクリプトでログイン情報を推測することです。ユーザーが常に強力なパスワードを使っているとは限らないため、十分時間をかけることができれば、攻撃者がスクリプトを使ってパスワードリストを元に多数の推測をかけ、それなりの数のユーザーのログイン情報を取得できる可能性があります。

アプリ側でリクエストの種類や回数を制限することで、この攻撃は困難になります。たとえばrack-attack gemは、リクエストをブロックしたり絞り込むのに便利なDSLを使えるRackミドルウェアです。

Railsチュートリアルのとおりに実装したアプリでリクエストを絞り込みたい場合、rack-attackをインストールしてから以下のように記述します。

throttle('req/ip', :limit => 300, :period => 5.minutes) do |req|
  req.ip
end

上のコード片は、5分間に300以上のリクエストを送信するすべてのIPアドレスを制限するようRack::Attackに伝えます。この設定ではリクエストオブジェクトを受信しますが、任意のリクエストパラメータを指定して絞り込むことも技術的に可能です。

この設定で制限されるのは、リクエストを多数送信する単一のIPアドレスですが、攻撃者は多くのIPアドレスを使い分けてユーザーのアカウントに総当たり攻撃をかけるかもしれません。こうした攻撃を緩和するには、次のようにアカウントごとのリクエスト絞り込みを検討するとよいでしょう。

throttle("logins/email", :limit => 5, :period => 20.seconds) do |req|
  if req.path == '/login' && req.post?
    req.params['email'].presence # this will return the email if present, and nil otherwise
  end
end

Deviseを使っているのであれば、ログイン失敗の繰り返しがあまりにも多いユーザーのアカウントをロックするオプションもあります。アカウントのロックアウトは自分で実装することもできます。

しかし、ユーザーアカウント単位でアカウントロックや絞り込みを行うと、今度は攻撃者がそれを逆手に取って任意のユーザーアカウントを強制的にロックするという嫌がらせを行うかもしれません。これはいわゆるDoS攻撃の一種です。これについて簡単には答えられません。アプリに何らかのセキュリティメカニズムを導入する場合、引き受けるリスクの種類や深刻さについて決断をくださなくてはなりません。以下は古いStackExchangeのQ&Aですが、こうした問題について考察を深めるのに役に立つかもしれません。

メモ: Rack::AttackとDDoS攻撃について

絞り込みを実装する前に、Rack::Attackのトラッキング機能でWebサーバーへのトラフィックの傾向を把握するとよいでしょう。こうして集めた情報は、パラメータの効果的な絞り込みに役立ちます。Rack::Attackの作者であるAaron Suggsは、トラッキング機能はiptablesやnginxのlimit_conn_zoneなどのようなセキュリティ測定を補完するものであると述べています。

DoS攻撃やDDoS(分散DoS)攻撃はそれだけで大きなテーマなので、興味の湧いた方はぜひ深いところまで追ってみることをおすすめします。また、Cloudflareなどのサービス設定ではどのようにDDoS攻撃を緩和しているかを調べてみるのもよいでしょう。

2. セキュリティヘッダーを正しく設定する

リクエストをHTTPSで保護していても、SSL strippingというタイプの中間者攻撃(Man in the Middle attack)に対して脆弱になっていることがあります。この攻撃について解説します。

カナダに住んでいる人がモントリオール銀行を利用しているとします。ある日、この人がメールで送金しようと思ってChromeブラウザのアドレスバーに「bmo.com」と入力します。Chromeのdeveloper toolで「Network」タブを開いてみるとわかると思いますが、このときの最初のリクエストはHTTPSではなく、HTTPになります。攻撃者が巧妙な方法でこの最初のHTTPリクエストを奪い取ることに成功すると、モントリオール銀行のWebサイトのふりをしてサービスを提供し、ログインさせてログイン情報を奪い取るなどの可能性があります。この攻撃は、ブラウザがHTTPSではなくHTTPを使っていなければ成功しません。

この攻撃を防止するためのヘッダーがStrict-Transport-Securityであり、HSTS (HTTP Strict Transport Security)ヘッダーとも呼ばれます。サーバーがこのヘッダーをレスポンスに含めることで、ブラウザでHTTPS以外の通信を行えないようにできます。通常、HSTSヘッダーにはmax-ageというパラメータが含まれており、ブラウザはこのパラメータの値(訳注: 単位は秒)をサーバーとの通信が許可される時間として認識します。max-ageが31536000(=1年間)の場合、ブラウザは1年間だけサーバーとHTTPSで通信します。サーバーはHSTSヘッダーを使って、ブラウザにサブドメインとの通信も許可するかどうかも指定できます。詳しくは以下をご覧ください。

RailsでHSTSヘッダーを使うには、config.force_ssl = trueを設定します。これでHSTSヘッダーが設定され、値は180.daysになります。この設定をすべてのサブドメインにも適用するには、config.ssl_options = {hsts: {subdomains: true}}とします。

しかしこの対策には抜け道があります。上述のように、最初のリクエストだけはHTTPになっている可能性が残されているからです。HSTSヘッダーは、最初のリクエストを除いてすべてのリクエストを保護します。

これについては、ユーザーがブラウザにHTTPSを付けずにURLを入力した場合でも、サイト側からブラウザにHTTPSを常に使わせる方法があります。それは、Chromium preload listというサイトに自社のドメインを追加することです。このpreloadリストにドメインを追加した場合の欠点は、そのドメインでは今後一切HTTP通信が使えなくなることです。

HSTSヘッダーがあるからといってユーザーが絶対安全になるわけではありませんが、それでもHSTSヘッダーを使わないよりは使う方が安全度は高まると考えています。

関心のある方は、securityheaders.ioで自分のサイトがどんなセキュリティ関連ヘッダー(またはどのHSTSヘッダー)をレスポンスで返しているかをチェックできます。ここですべてのヘッダーをチェックし、自分のサイトがこうした問題に該当するかどうかを学んでから判断することをおすすめします。

3. いろんな認証ライブラリのコードを読む(Devise、Authlogic、Clearance、Rodauthなど何でも)

この方法は特に認証システムを独自開発する場合に当てはまりますが、そうでない場合であっても、あるgemで行われていることが別のgemでも似たような感じで行われている様子を学べば大きな収穫を得られます。その際、常にソースコードを読まなければダメだなどと思い込む必要はありません。changelogやメンテナーのブログに掲載される更新情報記事を眺めるだけでもかなりの情報が得られます。メンテナーはしばしば、発見された脆弱性やそれらを緩和するための手順をそうした場で深く掘り下げているからです。ここでは、RodauthとDeviseから私が学んだ興味深い内容を3つご紹介します。

パスワードハッシュへのアクセス制限(Rodauth)

Rodauthでは、Deviseや多くの独自認証ライブラリの例と異なり、パスワードハッシュを保存するテーブルが完全に分離されており、このテーブルはアプリの他の場所からはアクセスできないようになっています。Rodauthではそのためにappph(訳注: password hashの略)という2つのデータベースアカウントを設定します。パスワードハッシュはphアカウントでアクセスできるテーブルだけに保存し、appアカウントの方では、パスワードハッシュが一致するかどうかをphアカウントで確認するデータベース関数にアクセスします。こうすることで、アプリにSQLインジェクションの脆弱性が発生したとしても、ユーザーのパスワードハッシュに直接アクセスすることはできなくなります。

ユーザー固有のトークン (Rodauth)

Rodauthでは、パスワードリセットなどの秘密トークンを独立したテーブルに保存するほかに、トークンの前にアカウントIDを追加します。たとえば、「パスワードをお忘れですか?」リンクが仮にwww.example.com/reset_password?reset_password_token=abcd1234のようになっていたとして、攻撃者がここから有効なトークンを推測しようとしているとします。このとき攻撃者は、全ユーザーに対して有効になりそうなトークンを推測するでしょう。トークンの前にアカウントIDを追加するとreset_password_token=<account_id>-abcd1234のような感じになり、攻撃者は1度に1人のユーザーについてしかトークンを推測できなくなります。

トークンのダイジェスト化(Devise)

数年前にリリースされたDevise 3.1から、パスワードのリセット・確認・アンロックのトークンをダイジェスト化してからデータベースに保存するようになりました。SecureRandomで生成されたトークンはOpenSSL::HMAC.hexdigestメソッドでダイジェスト化されます。

攻撃者がデータベースにアクセスしたときに備えてトークンを難読化するほかに、いわゆる時間差攻撃を防ぐ効果もあります。トークンがダイジェスト化されると、攻撃者がトークンを1バイトずつ変更して変更点を比較して文字列を制御することはほぼ不可能になります。

Rodauthについてもっと知りたい方は、RodauthのGithubリポジトリをご覧ください。Rodauthの作者であるJeremy Evansの講演動画もご覧になるとよいでしょう。

つまり、他の認証フレームワークで使われている認証方法や攻撃への対策を研究しておくことで、その分自信を持ってアプリの認証設定のセキュリティを調査できるようになるということです。

4. アプリの他の部分をセキュアにする

認証は単独で使われるものではありません。認証システムで講じられていたあらゆるセキュリティ対策をバイパスしてしまうような脆弱性は、アプリの他の部分にも潜んでいます。

クロスサイトスクリプティング(XSS)脆弱性が発生しているRailsアプリを例にとって考えてみましょう。コードベースのどこかで運悪くユーザー入力にhtml_safeを適用してしまったためにこの脆弱性が発生したとします。Railsのバージョンは4以降なので、cookieにはデフォルトでhttpOnlyが設定されており、インジェクションしようとしている攻撃者はdocument.cookieにアクセスできません。アプリはセッションハイジャックに対しては安全に見えますが、攻撃者がセッションを乗っ取る方法は他にもまだまだあります。たとえば、AJAXリクエストに含まれるユーザーのパスワードを変更するJavaScriptを注入する方法があります。パスワードの変更時に現在のパスワードの入力が必須になっている場合は、攻撃者はユーザーのメールアドレスも(自分のものに)書き換えてパスワードリセットフローを開始するでしょう。

XSS脆弱性は、認証システムがセキュアであるかどうかとは無関係です。同様のことは、ディレクトリトラバーサルクロスサイトリクエストフォージェリ(CSRF)といった他の脆弱性についても言えます。

セキュリティの脆弱性について学んだ後、そこで得た知識を元に自分のアプリを実際に攻撃してみるのは、長い目で見てセキュアなコードを書けるようになるためのよい方法です。Railsセキュリティガイドや以下のようなセキュリティチェックリストに目を通すこともおすすめします。

最後に

ちょっとだけ宣伝: カナダのトロントまで飛行機で数時間程度のところにお住まいでしたら、個人・会社を問わずセキュリティについて無料でお話しいたします。詳しくは「sidkアットマークducktypelabsドットcom」までどうぞ。

この記事のリストはセキュリティについてすべてを網羅しているわけではありません(すべてを書いたら本数冊分にはなるでしょう)。しかし、この記事がセキュリティについて少しでも深く考える機会となり、アプリのセキュリティを高める方法をこの記事から1つでも学んでいただければと願っています。

(元記事の)末尾にコメント欄がありますので、ぜひコメントをお寄せください。皆さんはどうやって認証をセキュアにしていますか?

関連記事(Duck Type Lab翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ

BPSアドベントカレンダー