Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: 5 security issues in Ruby on Rails apps from real life - frontdeveloper.pl 原文公開日: 2018/10/03(10/09更新) 著者: Igor Springer Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳) 私は長年に渡って、Ruby on Railsアプリでさまざまなセキュリティ問題を発見・修正する機会が何度もありました。この経験を元に、皆さんのRailsアプリをさらにセキュアにする方法をご紹介いたします。ここでご紹介するセキュリティ問題が皆さまのアプリで発生していなければ幸いです。 Rails組み込みのセキュリティ機能について詳しくは、公式のRailsセキュリティガイドをご覧ください。 1. 「セッションに期限を設定していない」問題 セキュリティ問題の解説 「Railsセキュリティガイド」には以下のように記載されています。 セッションを無期限にすると、攻撃される機会を増やしてしまいます (クロスサイトリクエストフォージェリ (CSRF)、セッションハイジャック、セッション固定など)。 ユーザーエクスペリエンスを考慮すれば(ユーザーはずっとサインインしたままになるので、アプリを開くたびにサインインしなくても済む)、無期限セッションの方が正しいアプローチと思われそうですが、無期限セッションは悪手です。公共の場所に置かれたPCでユーザーがアプリからサインアウトし忘れた場合に、何者かにユーザーセッションをハイジャックされる状況を防止するためには、セッションをできる限り早期に無効にするべきです。 解決方法 最も単純な解決方法は、config/initializers/session_store.rbで以下のようにセッションcookieの有効期限を設定することです。 Rails.application.config.session_store :cookie_store, expire_after: 12.hours これによって、セッションcookieは作成後12時間で期限が切れます。この方法は実装が容易なのですが、実は大きな欠点があります。有効期限はユーザーのブラウザに設定されるので、セッションcookieにアクセスできさえすれば誰でもcookieを書き換えて期限を簡単に延長できてしまいます。 よりセキュアな方法でこの問題を解決するには、セッションの有効期限をサーバー側に保存すべきです。この方法は「Railsセキュリティガイド」でも提案されています。 セッションIDを持つcookieのタイムスタンプに有効期限を設定するという対応策も考えられなくはありません。しかし、ブラウザ内に保存されているcookieをユーザーが編集できてしまう点は変わらないので、やはりサーバー側でセッションを期限切れにする方が安全です。 ユーザー認証にDevise gemを使っているRailsアプリの場合、Timeoutableモジュールが組み込まれます。このモジュールは、ユーザーセッションが期限切れかどうかの検証を行います。これを利用するには、アプリでユーザーを表すモデルの中でこのモジュールを次のように有効にする必要があります。 class User < ActiveRecord::Base devise :timeoutable end 続いてdeviseイニシャライザのtimeout_inオプションに必要な値(デフォルトは30分)を設定します。 # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. # After this time the user will be asked for credentials again. # Default is 30 minutes. config.timeout_in = 30.minutes Devise gemを使っていない場合は、Sessionモデルを作成してcreated_atタイムスタンプとupdated_atタイムスタンプをそこに保存し、古くなったレコードはそこから削除するようにしておきます。繰り返しになりますが、このコード例は「Railsセキュリティガイド — セッションの期限切れ」にあります。 2018/10/09原文更新: InCaseOfEmergency氏からのredditのコメントで、組み込みのソリューションがRails 5.2で改善され、期限切れタイムスタンプがセッションcookieの一部に含まれるようになったとの情報をいただきました。素晴らしい! 2. 「アカウントロックの仕組みがない」問題 セキュリティ問題の解説 1人のユーザーが何回サインインに失敗したらユーザーを無効にしますか?失敗回数を「無制限」にすると、セキュリティホールが生じます。ユーザーがメールアドレスとパスワードをさまざまに変えてサインインしようとすることを許すと、攻撃者にそれを許したも同然です。辞書攻撃や総当たり攻撃を準備するスクリプトを使って、ものの数分で突破されてしまうでしょう。 総当たり攻撃: ユーザー名/パスワードのあらゆる組み合わせを試す攻撃 辞書攻撃: ありがちなパスワードのリストを元に推測する攻撃 この問題を修正するには、間違ったユーザー名/パスワードの組み合わせを入力できる上限回数を設定し、それを超えたらユーザーをブロックすべきです。 解決方法 Devise gemを使っていれば先ほどと同様に話は簡単です。Deviseには、サインイン試行回数を超えたユーザーをブロックできるLockableモジュールがあります。回数は自由に設定できますが、まずは5回に設定しておくのがよいでしょう。ユーザーから不満の声が上がったときに、いつでも値を変更できます。 Lockableモジュールは以下の2つのアンロック戦略を提供します。 :time: 指定の時間が経過したらユーザーのブロックを自動解除する。 :email: ユーザーがロックされた場合にロック解除のリンクをメールで通知する。 この2つの戦略はロック対策を何も行わないよりずっとましですが、最終的にどちらを選ぶかはあなた次第です。Deviseでは両方を同時に使うこともできます。詳しくはDeviseの公式ドキュメントをどうぞ。 Devise gemを使っていない場合は、自力で同じようなソリューションを実装できます(コードはネット上にいろいろあります🙂)。今使っているライブラリに同じようなソリューションがあるかどうかチェックするとよいでしょう。 CAPTCHAを実装することで総当たり攻撃や辞書攻撃の防止に役立てることもできます 出典: https://hakiri.io/blog/rails-login-securityより 3. 「ユーザーリストを取られる」「メールアドレスを推測される」問題 セキュリティ問題の解説 さほど問題には見えないかもしれませんが、深刻な問題です。試しに自分のアプリでめったに使われない「パスワードをリセットする」ページを開いてみてください。 「入力したメールアドレスのユーザーは存在しません」といううかつなバリデーションエラーメッセージが表示されなければ幸いです。このメッセージがよくない理由はおわかりでしょうか?攻撃者がこれを使って、そのシステムに存在するメールアドレスのリストを集めることができてしまう可能性があるからです。 既存のメールアドレスのリストを数百万件のリクエストとしてアプリに送信し、その結果を元にリストのどのユーザーが本当にそのシステムに存在するかをチェックするスクリプトは実に簡単に作れます。もしロックアウトのしくみがなければ、実在ユーザーのメールアドレスリストを手に入れた攻撃者が上述のセキュリティホールを衝いてユーザーアカウントへのアクセスを奪取するかもしれません。 解決方法 ユーザーが入力するメールアドレスがアプリのユーザーに割り当てられたものであろうと、ランダムなメールアドレスであろうと、アプリは同じレスポンスを返すべきです(APIからのJSONレスポンスか、確認メッセージを表示するページへのリダイレクトで)。こうすることで、攻撃者はアプリのユーザーのメールアドレスのリストを取れなくなります。 Devise gemを使っている場合は、コードのコメントにあるparanoidオプションが使えます。 入力したメールアドレスが正しいかどうかにかかわらず、確認やパスワード復元といったワークフローの振る舞いを同じにする。 Deviseコメントより抜粋 Deviseを使っていない場合は、入力したメールアドレスが正しいかどうかにかかわらず振る舞いが同じになるよう、自分でアプリを調整すべきです。 このようなレスポンスを返すと、`non-existing-email@domain.come-mail`というメールアドレスがどのユーザーにも使われていないことが攻撃者に知られてしまい、次のアドレスを試されてしまう可能性があります。 メールアドレスが漏洩せず、かつ明確なメッセージ(正確には上下2つのメッセージ) Patron did not receive Password Reset Emailより 4.「権限昇格(権限のないリソースへのアクセス)」の問題 訳注: 週刊Railsウォッチの「Rails初心者とバレる書き方」もどうぞ。 セキュリティ問題の解説 権限昇格は起きてはならないことですが、起きるときは起きます。あなたが仮に、ユーザーのプロジェクトをIDで参照できる、新しいAPIエンドポイントを作成したとしましょう。 GET https://my-rails-app.com/api/projects/:project_id テストユーザーに割り当てたいくつかのプロジェクトIDを使うcUrlリクエストをいくつか試し、プロジェクトの詳細を含むJSONペイロードが首尾よく返されたので、ようやくこのエンドポイントをproductionにデプロイしました。しかしちょっと待った!別のユーザーに割り当てられたプロジェクトIDを使ってリクエストをこしらえたらどうなりますか? 残念でした。プロジェクトへのアクセスをcurrent_userに限定するのを忘れていたのです。 公式のRailsガイドには、このセキュリティ問題をまとめた秀逸な一文があります。 ユーザー入力は安全確認が終わるまではセキュアではなく、ユーザーから送信されるどのようなパラメータにも、何らかの操作が加えられている可能性が常にあります 。 Rails セキュリティガイド | Rails ガイドより「6.7 権限昇格」 解決方法 いかなる場合も、アクセスを可能な限り絞り込むことを忘れてはいけません。自分のアプリのコントローラでcurrent_userメソッドにアクセスできるのであれば、以下のように置き換えて修正します。 Project.find(params[:id]) 上を以下のように置き換えます。 current_user.projects.find(params[:id]) 複数のリソースへのアクセスをオブジェクト指向的に制御したい場合は、pundit gemかcancancan gemを利用できます。 私はpunditの方が使い慣れています。また、punditにはdevelopment環境で常に使用すべき便利な機能が1つあります。たとえば、他のコントローラに継承されるメインのコントローラに以下のフィルタを追加できます。 after_action :verify_authorized こうしておけば、コントローラのアクションでauthorizeメソッド(リソースへのアクセスを基本的に制限する)を呼び忘れたときにpunditが知らせてくれるので、コードをリポジトリにプッシュする前でも対応できます。 punditもcancancanも使ったことのない方には、ぜひお試しいただくことをおすすめします。 5. 「弱いパスワードを排除していない」問題 セキュリティ問題の解説 アプリユーザーの大多数は1passwordもKeePassも使っていません。これらはセキュアなパスワードを生成して安全に保存し、サインインのたびに自動入力してくれます。 覚えやすいパスワードをあらゆるアプリで使いまわしているのが平均的なユーザー像です。 私の個人的な意見ですが、たとえユーザーに少々不便を強いることがあっても、ユーザーを指導してセキュリティに配慮することが私たち開発者の義務だと思います。 くれぐれも12345678やqwertyなどという弱々しいパスワードの作成をユーザーに許してはいけません。このようなパスワードが使われていると、簡単に総当たり攻撃や辞書攻撃の餌食になってしまいます。 … Continue reading Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)