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を使っていない場合は、自力で同じようなソリューションを実装できます(コードはネット上にいろいろあります🙂)。今使っているライブラリに同じようなソリューションがあるかどうかチェックするとよいでしょう。
🔗 3.「ユーザーリストを取られる」「メールアドレスを推測される」問題
🔗 セキュリティ問題の解説
さほど問題には見えないかもしれませんが、深刻な問題です。試しに自分のアプリでめったに使われない「パスワードをリセットする」ページを開いてみてください。
「入力したメールアドレスのユーザーは存在しません」といううかつなバリデーションエラーメッセージが表示されなければ幸いです。このメッセージがよくない理由はおわかりでしょうか?攻撃者がこれを使って、そのシステムに存在するメールアドレスのリストを集めることができてしまう可能性があるからです。
既存のメールアドレスのリストを数百万件のリクエストとしてアプリに送信し、その結果を元にリストのどのユーザーが本当にそのシステムに存在するかをチェックするスクリプトは実に簡単に作れます。もしロックアウトのしくみがなければ、実在ユーザーのメールアドレスリストを手に入れた攻撃者が上述のセキュリティホールを衝いてユーザーアカウントへのアクセスを奪取するかもしれません。
🔗 解決方法
ユーザーが入力するメールアドレスがアプリのユーザーに割り当てられた有効なものであっても、ランダムなメールアドレスであっても、アプリは同じレスポンスを返すべきです(APIからのJSONレスポンスか、確認メッセージを表示するページへのリダイレクトで)。こうすることで、攻撃者はアプリのユーザーのメールアドレスのリストを取れなくなります。
Devise gemを使っている場合は、コードのコメントにあるparanoid
オプションが使えます。
入力したメールアドレスが正しいかどうかにかかわらず、確認やパスワード復元といったワークフローの振る舞いを同じにする。
Deviseコメントより抜粋
Deviseを使っていない場合は、入力したメールアドレスが正しいかどうかにかかわらず振る舞いが同じになるよう、自分でアプリを調整すべきです。
このようなレスポンスを返すと、non-existing-email@domain.come-mail
というメールアドレスがどのユーザーにも使われていないことが攻撃者に知られてしまい、次のアドレスを試されてしまう可能性があります。
🔗 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
などという弱々しいパスワードの作成をユーザーに許してはいけません。このようなパスワードが使われていると、簡単に総当たり攻撃や辞書攻撃の餌食になってしまいます。
🔗 解決方法
パスワードポリシーを導入し、適用しましょう。
パスワードポリシーとは、強いパスワードの利用をユーザーに促して正しく運用することでコンピュータのセキュリティを高める目的で制定されたルールのセットのことである。
Password policy - Wikipediaより
基本的なポリシーは、User
モデルに以下のカスタムバリデーションメソッド追加するだけで適用できます。
validate :password_complexity
def password_complexity
return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/
errors.add :password, "パスワードの強度が不足しています。パスワードの長さは8〜70文字とし、大文字と小文字と数字と特殊文字をそれぞれ1文字以上含める必要があります。"
end
上のメソッドはDeviseのwikiから引用したものです。本記事を読んだ方は一刻も早くアプリのコードにこれを追加すべきです。
オーバーキル気味かもしれませんが🙂、もっと細かく制限したい方は、strong_password gemをチェックするとよいでしょう。
🔗 まとめ
Ruby on Railsアプリでこれまで発生した5つのセキュリティ問題をご紹介しました。これらは今後も発生する可能性があります。
本記事をお読みいただいている皆さまのアプリに、これらの問題が1つもないことを願っています。修正可能な問題が見つかった方が、本記事でご紹介したソリューションを活用してセキュリティホールを塞ぐことができれば幸いです。グッドラック🙂!
リンク集
- The official OWASP Ruby on Rails security checklist
- Rails Security Checklist
- The SaaS CTO Security Checklist
概要
原著者の許諾を得て翻訳・公開いたします。
週刊Railsウォッチ20181015『Railsアプリで実際に起きたセキュリティ問題5つ』もどうぞ。