#前提
本日学んだセキュリティーについて書いていきます。
#本題
#リダイレクトとファイル
セキュリティ上の脆弱性として検討したいのは、Webアプリケーションにおける「リダイレクトとファイル」。
##リダイレクト
Webアプリケーションにおけるリダイレクトは、過小評価されがちなクラッキングツール。
攻撃者はこれを使ってユーザーを危険なWebサイトに送り込んだり、Webサイト自体に罠を仕掛けたりすることもできる。
リダイレクト用のURL (の一部) を渡すことをユーザーに許すと、潜在的な脆弱性となる。
最もあからさまな攻撃方法としては、ユーザーを本物そっくりの偽Webサイトにリダイレクトすることが考えられる。
これは俗に「フィッシング(phishing)」や「釣り」などと呼ばれる攻撃手法。
具体的には、無害を装ったリンクを含むメールをユーザーに送りつけ、XSSを使ってそのリンクをWebアプリケーションに注入するか、リンクを外部サイトに配置する。
このリンクの冒頭部分はそのWebアプリケーションのURLなので、一見無害に見える。
##ファイルアップロード
ファイルがアップロードされたときに重要なファイルが上書きされることのないようにする。
また、メディアファイルの処理は非同期で行なう。
多くのWebアプリケーションでは、ユーザーがファイルをアップロードできるようになっている。
ユーザーが選択/入力できるファイル名 (またはその一部) は必ずフィルタする。
攻撃者が危険なファイル名をわざと使ってサーバーのファイルを上書きしようとする可能性があるため。
ファイルが /var/www/uploads ディレクトリにアップロードされ、そのときにファイル名が「../../../etc/passwd」と入力されていると、重要なファイルが上書きされてしまう可能性がある。
言うまでもなく、Rubyインタプリタにそれだけの実行権限が与えられていなければ、そのような上書きは実行できない。
Webサーバー、データベースサーバーなどのプログラムは、比較的低い権限を持つUnixユーザーとして実行されているのが普通。
さらにもう一つ注意。
ユーザーが入力したファイル名をフィルタするときに、ファイル名から危険な部分を取り除くアプローチを使わないこと。
Webアプリケーションがファイル名から「../」という文字を取り除くことができるとしても、今度は攻撃者が「....//」のようなその裏をかくパターンを使えば、やはり「../」という相対パスが通ってしまい、きりがない。
最も良いのは「ホワイトリスト」によるアプローチ。
これはファイル名が有効であるかどうか (指定された文字だけが使われているかどうか) をチェックするもの。
これは「ブラックリスト」アプローチと逆の手法であり、利用が許されてない文字を除去する。
ファイル名が無効の場合は、拒否するか、無効な文字を置き換えますが、取り除くわけではない。
##ファイルアップロードで実行可能なコードを送り込む
アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれると、ソースコードが実行可能になってしまう可能性がある。
Railsの/publicディレクトリがApacheのホームディレクトリになっている場合は、ここにアップロードファイルを置いてはいけない。
広く使われているApache WebサーバーにはDocumentRootというオプションがある。
これはWebサイトのホームディレクトリであり、このディレクトリツリーに置かれているものはすべてWebサーバーによって取り扱われる。
そこに置かれているファイルの名前に特定の拡張子が与えられていると、それに対してリクエストが送信された時に実行されてしまうことがある。
実行される可能性のある拡張子は、たとえばPHPやCGIなど。
攻撃者が「file.cgi」というファイルをアップロードし、その中に危険なコードが仕込まれているとする。
このファイルを誰かがダウンロードすると、このコードが実行される。
ApacheのDocumentRootがRailsの/publicディレクトリを指している場合、アップロードファイルをここに置かない。
少なくとも1階層上に保存する必要がある。
##ファイルのダウンロード
ユーザーが任意のファイルをダウンロードできる状態を作らないこと。
ファイルアップロード時にファイル名のフィルタが必要になるのと同様、ファイルのダウンロード時にもファイル名をフィルタする必要がある。
以下のsend_file()メソッドは、サーバーからクライアントにファイルを送信します。フィルタ処理されていないファイル名を使うと、ユーザーが任意のファイルをダウンロードできるようになってしまう。
send_file('/var/www/uploads/' + params[:filename])
「../../../etc/passwd」のようなファイル名を渡せば、サーバーのログイン情報をダウンロードできてしまう。
これに対するシンプルな対応策は、リクエストされたファイル名が、想定されているディレクトリの下にあるかどうかをチェックすること。
その他に、ファイル名をデータベースに保存しておき、データベースのidをサーバーのディスク上に置く実際のファイル名の代りに使う方法も併用できる。
この方法も、アップロードファイルが実行される可能性を回避する方法として優れている。
attachment_fuプラグインでも同様の手法が採用されている。
#イントラネットAdminのセキュリティ
イントラネットおよび管理画面インターフェイスは、強い権限が許されているため、何かと攻撃の目標にされがち。
イントラネットおよび管理画面インターフェイスには、他よりも手厚いセキュリティ対策が必要ですが、現実には逆にむしろこれらの方がセキュリティ対策が薄いということがしばしばある。
イントラネットや管理アプリケーションにとって最も脅威なのはXSSとCSRF。
XSS: 悪意のあるユーザーがイントラネットの外から入力したデータがWebアプリケーションで再表示されると、WebアプリケーションがXSS攻撃に対して脆弱になる。
ユーザー名、コメント、スパムレポート、注文フォームの住所のような情報すらXSS攻撃に使われることがある。
管理画面やイントラネットで1箇所でもサニタイズ漏れがあれば、アプリケーション全体が脆弱になる。
想定される攻撃としては、管理者のcookieの盗み出し、管理者パスワードを盗み出すためのiframe注入、管理者権限奪取のためにブラウザのセキュリティホールを経由して邪悪なソフトウェアをインストールする、などが考えられる。
CSRF: クロスサイトリクエストフォージェリ (Cross-Site Request Forgery) はクロスサイトリファレンスフォージェリ (XSRF: Cross-Site Reference Forgery) とも呼ばれ、非常に強力な攻撃手法。
この攻撃を受けると、管理者やイントラネットユーザーができることをすべて行えるようになってしまう。
RailsのURLはかなり構造が素直であるため、オープンソースの管理画面を使っていると構造を容易に推測できてしまう。
攻撃者は、ありそうなIDとパスワードの組み合わせを総当りで試す危険なImageタグを送り込むだけで、数千件ものまぐれ当たりを獲得することもある。
###その他予防策
管理画面は、多くの場合次のような作りになっている。www.example.com/admin のようなURLに置かれ、Userモデルのadminフラグがセットされている場合に限り、ここにアクセスできる。
ユーザー入力が管理画面で再表示されると、管理者の権限でどんなデータでも削除/追加/編集できてしまう。
常に最悪の事態を想定することは極めて重要。
「誰かが自分のcookieやユーザー情報を盗み出すことができたらどうなるか」。
管理画面にロール (role)を導入することで、攻撃者が行える操作の範囲を狭めることができる。
1人の管理者に全権を与えるのではなく、権限を複数管理者で分散する方法や、管理画面用に特別なログイン情報を別途設置するという方法もある。
一般ユーザーが登録されているUserモデルに管理者も登録し、管理者フラグで分類していると攻撃されやすいことから、これを避けるため。
極めて重要な操作では別途特殊なパスワードを要求する方法もある。
管理者は、必ずしも世界中どこからでもそのWebアプリケーションにアクセスできる必要性はないはず。
送信元IPアドレスを一定の範囲に制限するという方法。request.remote_ipメソッドを使えばユーザーのIPアドレスをチェックできる。
この方法は攻撃に対する直接の防御にはならないが、検問としては非常に有効。
ただし、プロキシを用いて送信元IPアドレスを偽る方法がある。
管理画面を特別なサブドメインに置き ( admin.application.com など)、さらに管理アプリケーションを独立させてユーザー管理を独自に行えるようにする。
このような構成にすることで、通常の www.application.com ドメインからの管理者cookieを盗み出すことは不可能。
ブラウザには同一生成元ポリシーがあるので www.application.com に注入されたXSSスクリプトからはadmin.application.comのcookieは読み出せず、逆についても同様に読み出し不可となる。
#ユーザー管理
認証 (authentication) と認可 (authorization) はほぼすべてのWebアプリケーションにおいて不可欠。
認証システムは自前で作るよりも、広く使われているプラグイン (訳注: 現在ならgem) を使うべき。
ただし、常に最新の状態にアップデートするようにする。
Railsでは多数の認証用プラグインを利用できる。
人気の高いdeviseやauthlogicなどの優れたプラグインは、パスワードを平文ではなく常に暗号化した状態で保存する。
Rails 3.1では、同様の機能を持つビルトインのhas_secure_passwordメソッドを使える。
新規ユーザーは必ずメール経由でアクティベーションコードを受け取り、メール内のリンク先でアカウントを有効にするようになっている。
アカウントが有効になると、データベース上のアクティベーションコードのカラムはNULLに設定される。
以下のようなURLをリクエストするユーザーは、データベースで見つかる最初に有効になったユーザーとしてWebサイトにログインできてしまう可能性がある。そしてそれがたまたま管理者である可能性もありえる。
http://localhost:3006/user/activate
http://localhost:3006/user/activate?id=
一部のサーバーでは、params[:id]で参照されるパラメータidがnilになってしまっていることがあるため、上のURLが通用してしまう可能性がある。アクティベーション操作中にこのことが敵に突き止められるまでの流れは以下のとおり。
User.find_by_activation_code(params[:id])
パラメータがnilの場合、以下のSQLが生成される。
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
この結果、データベースに実在する最初のユーザーが検索で見つかり、結果が返されてログインされてしまう。
##アカウントに対する総当たり攻撃
アカウントに対する総当たり攻撃 (Brute-force attack) とは、ログイン情報に対して試行錯誤を繰り返す攻撃。
エラーメッセージを具体的でない、より一般的なものにすることで回避可能 だが、CAPTCHA (相手がコンピュータでないことを確認するためのテスト) への情報入力の義務付けも必要。
Webアプリケーション用のユーザー名リスト (名簿) は、パスワードへの総当たり攻撃に悪用される可能性がある。
パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすい。
辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくある。
従って、名簿と辞書を使って総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られている。
このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションではわざと具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしている。
ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせる。
「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを表示したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成する。
しかし、Webアプリケーションのデザイナーがおろそかにしがちなのは、いわゆる「パスワードを忘れた場合」ページ。
こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示される。
こうした情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまう。
これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示するようにする。
さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHAの入力をユーザーに義務付けるようにする。
もちろん、この程度では自動化された総当たり攻撃プログラムからの攻撃から完全に逃れることはできない。
こうしたプログラムは送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるから。
しかしこの対策は攻撃に対するある程度の防御になることも確か。
##アカウントのハイジャック
多くのWebアプリケーションでは、ユーザーアカウントを簡単にハイジャックできてしまう。
###パスワード
攻撃者が、盗み出されたユーザーセッションcookieを手に入れ、それによってWebアプリケーションが標的ユーザーとの間で共用可能になった状態を考えてみる場合。
パスワードが簡単に変更できる画面設計(古いパスワードの入力が不要)であれば、攻撃者は数クリックするだけでアカウントをハイジャックできてしまう。
あるいは、パスワード変更画面がCSRF攻撃に対して脆弱な作りになっている場合、攻撃者は標的ユーザーを別のWebページに誘い込み、CSRFを実行するように仕込まれたimgタグを踏ませて、標的ユーザーのWebパスワードを変更する。
対応策としては、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすること。
同時に、ユーザーにパスワードを変更させる場合は、古いパスワードを必ず入力させること。
###メール
しかし攻撃者は、登録されているメールアドレスを変更することでアカウントを乗っ取ろうとする可能性もある。
攻撃者は、メールアドレス変更に成功すると「パスワードを忘れた場合」ページに移動し、攻撃者の新しいメールアドレスに変更通知メールを送信する。
システムによってはこのメールに新しいパスワードが記載されていることもある。
対応策は、メールアドレスを変更する場合にもパスワード入力を必須にすること。
###その他
Webアプリケーションの構成によっては、ユーザーアカウントをハイジャックする方法が他にも潜んでいる可能性がある。
多くの場合、CSRFとXSSが原因となる。
GMailのCSRF脆弱性で紹介されている例をとりあげる。
同記事の概念実証によると、この攻撃を受けた場合、標的ユーザーは攻撃者が支配するWebサイトに誘い込まれる。
そのサイトのImgタグには仕掛けがあり、GMailのフィルタ設定を変更するHTTP GETリクエストがそこから送信される。
この標的ユーザーがGMailにログインしていた場合、フィルタ設定が攻撃者によって変更され、この場合はすべてのメールが攻撃者に転送されるようになる。
この状態は、アカウント全体がハイジャックされたのと同じぐらいに有害。
対応策は、アプリケーションのロジックを見なおしてXSSやCSRF脆弱性を完全に排除すること。
##CAPTCHA
CAPTCHAとは、コンピュータによる自動応答でないことを確認するためのチャレンジ-レスポンス式テスト。
コメント入力欄などで、歪んだ画像に表示されている文字を入力させることで、入力者が自動スパムボットでないことを確認する場合によく使われる。
ネガティブCAPTCHAという手法を使えば、入力者に自分が人間であることを証明させるかわりに、ボットを罠にはめて正体を暴くことができる。
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タイムスタンプを含めたフィールドをフォームに含めておき、サーバー側でこのフィールドをチェックする。
フィールドの時刻が遠い過去や未来の時刻であれば、そのフォームは無効。
フィールド名をランダムに変更します
送信ボタンを含むあらゆる型の数だけハニーポットフィールドを複数用意。
この方法で防御できるのは自動ボットだけであり、狙いを定めて特別に仕立てられたボットは防げない。
つまり、ネガティブキャプチャはログインフォームの保護には必ずしも向いているとは限らない。
##ログ出力
パスワードをRailsのログに出力しないこと。
デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力される。
しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれていることがあるため、重大なセキュリティ問題の原因になることがある。
Webアプリケーションのセキュリティコンセプトを設計するときには、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合のことも必ず考慮に含めておく必要がある。
パスワードや機密情報をログファイルに平文のまま出力してしまうと、データベース上でこれらの情報を暗号化する意味がなくなってしまう。
Railsアプリケーションの設定ファイル config.filter_parameters に特定のリクエストパラメータをログ出力時にフィルタする設定を追加できる。
フィルタされたパラメータはログ内で[FILTERED]という文字に置き換えられる。
config.filter_parameters << :password
指定したパラメータは正規表現の「部分マッチ」によって除外される。
Railsはデフォルトで:passwordを適切なイニシャライザ(initializers/filter_parameter_logging.rb)に追加し、アプリケーションの典型的なpasswordパラメータやpassword_confirmationパラメータに配慮する。
##正規表現
Rubyの正規表現で落とし穴になりやすいのは、より安全な\Aや\zがあることを知らずに危険な^や$を使ってしまうこと。
Rubyの正規表現では、文字列の冒頭や末尾にマッチさせる方法が他の言語と若干異なる。
このため、多くのRuby本やRails本でもこの点について間違った記載がある。
たとえば、URL形式になっているかどうかをざっくりと検証するために、以下のような単純な正規表現を使ったとする。
/^https?:\/\/[^\n]+$/i
これは一部の言語では正常に動作する。
しかし、Rubyでは^や$は、入力全体の冒頭と末尾ではなく、「 行の」冒頭と末尾にマッチしてしまう。
従って、この場合以下のような毒入りURLはフィルタを通過してしまう。
javascript:exploit_code();/*
http://hi.com
*/
上のURLがフィルタに引っかからないのは、入力の2行目にマッチしてしまうため。
従って、1行目と3行目にどんな文字列があってもフィルタを通過してしまう。
フィルタをすり抜けてしまったURLが、今度はビューの以下の箇所で表示されたとする。
link_to "Homepage", @user.homepage
表示されるリンクは一見無害に見えますが、クリックすると、攻撃者が送り込んだ邪悪なJavaScript関数を初めとするJavaScriptコードが実行されてしまう。
これらの正規表現に含まれる危険な^や$は、安全な\Aや\zに置き換える必要がある。
/\Ahttps?:\/\/[^\n]+\z/i
^や$をうっかり使ってしまうミスが頻発したため、Railsのフォーマットバリデータ(validates_format_of) では、正規表現の冒頭の^や末尾の$に対して例外を発生するようになった。
めったにないと思われるが、\Aや\zの代りに^や$をどうしても使いたい場合は、:multilineオプションをtrueに設定することもできる。
# この文字列のどの行にも"Meanwhile"という文字が含まれている必要がある
validates :content, format: { with: /^Meanwhile$/, multiline: true }
この機能は、フォーマットバリデータ利用時に起きがちなミスから保護するだけのものであり、それ以上のものではない点にご注意。
^や$はRubyでは 1つの行 に対してマッチし、文字列全体にはマッチしないということを開発者が十分理解しておくことが重要。
##権限昇格
パラメータが1つ変更されただけでも、ユーザーが不正な権限でアクセスできるようになってしまうことがある。
パラメータは、たとえどれほど難読化し、隠蔽したとしても、変更される可能性が常にあることを肝に銘じる。
改ざんされる可能性が高いパラメータといえばid。http://www.domain.com/project/1の1がid。
このidはコントローラのparamsを経由して取得できる。
コントローラ内では多くの場合、次のようなコードが使われている可能性がある。
@project = Project.find(params[:id])
このコードで問題がないWebアプリケーションもあるにはあるが、そのユーザーがすべてのビューを参照する権限を持っていない場合には問題となる。
このユーザーがURLのidを42に変更し、本来のidでは表示できないページを表示できてしまうため。
このようなことにならないよう、ユーザーのアクセス権も必ずクエリに含める。
@project = @current_user.projects.find(params[:id])
Webアプリケーションによっては、ユーザーが改ざん可能なパラメータが他にも潜んでいる可能性がある。
要するに、安全確認が終わっていないユーザー入力が安全である可能性はゼロであり、ユーザーから送信されるいかなるパラメータであっても、何らかの操作が加えられている可能性が常にあるということ。
難読化とJavaScriptによる検証のセキュリティだけでお茶を濁してはいけない。
ブラウザのWeb Developer Toolbarを使えば、フォームの隠しフィールドを見つけて変更することもできる。
JavaScriptを使ってユーザーの入力データを検証することはできても、攻撃者が想定外の値を与えて邪悪なリクエストを送信することは阻止しようがない。
Mozilla Firefox用のFirebugアドオンを使えば、すべてのリクエストをログに記録して、リクエストを繰り返し送信することも、リクエストを変更することもできてしまう。
さらに、JavaScriptによる検証はブラウザのJavaScriptをオフにするだけで簡単にバイパスできてしまう。
さらに、クライアントやインターネットのあらゆるリクエストやレスポンスを密かに傍受するプロキシがクライアント側に潜んでいる可能性すらある。
#インジェクション
インジェクション (注入) とは、Webアプリケーションに邪悪なコードやパラメータを導入して、そのときのセキュリティ権限で実行させること。
XSS (クロスサイトスクリプティング) やSQLインジェクションはインジェクションの顕著な例。
インジェクションによって注入されるコードやパラメータは、あるコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害。
その意味で、インジェクションは非常にトリッキーであると言える。
ここでいうコンテキストとは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどがある。
##ホワイトリスト方式とブラックブラックリスト方式
通常、サニタイズや保護や検証では、ブラックリスト方式よりもホワイトリスト方式が望ましい方法。
ブラックリストに使われるのは、有害なメールアドレス、publicでないアクション、邪悪なHTMLタグなど。
ホワイトリストはこれと真逆で、有害ではないメールアドレス、publicなアクション、無害なHTMLタグなどがホワイトリストになる。
スパムフィルタなど、対象によってはホワイトリストを作成しようがないこともあるが、基本的にホワイトリスト方式を使う。
セキュリティに関連するbefore_actionでは、except: [...]ではなくonly: [...]を使う。
なぜなら将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済むため。
クロスサイトスクリプティング (XSS) 対策として」という文字列の攻撃能力は失われていない。
だからこそ、ホワイトリストを用いるフィルタリングをおすすめする。
ホワイトリストによるフィルタは、Rails 2でアップデートされたsanitize()メソッドで使われている。
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))
この方法なら指定されたタグのみが許可されるため、あらゆる攻撃方法や邪悪なタグに対してフィルタが健全に機能する。
第2段階として、Webアプリケーションからの出力をもれなくエスケープすることが優れた対策。これは特に、ユーザー入力の段階でフィルタされなかった文字列がWeb画面に再表示されてしまうようなことがあった場合に有効。escapeHTML() (または別名のh()) メソッドを用いて、HTML入力文字「&」「"」「<」「>」を、無害なHTML表現形式(&、"、<、>) に置き換える。
###攻撃の難読化とエンコーディングインジェクション
従来のネットワークトラフィックは西欧文化圏のアルファベットがほとんどであったが、それ以外の言語を伝えるためにUnicodeなどの新しいエンコード方式が使われるようになってきた。
しかしこれはWebアプリケーションにとっては新たな脅威となるかもしれない。
異なるコードでエンコードされた中に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜んでいるかもしれないため。UTF-8による攻撃方法の例。
<IMG SRC=javascript:a
lert('XSS')>
上の例を実行するとメッセージボックスが表示される。
なお、これは上のsanitize()フィルタで認識される。
Hackvertorは文字列の難読化とエンコードを行なう優れたツールであり、「敵を知る」のに最適。
Railsのsanitize()メソッドは、このようなエンコーディング攻撃をかわす。
##CSSインジェクション
CSSインジェクションは実際にはJavaScriptのインジェクション。
MySpace Samyワームは、攻撃者であるSamyのプロファイルページを開くだけで自動的にSamyに友達リクエストを送信するというもの。
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"という単語をフィルタしていたにもかかわらず、「javascript」と書くことでこのフィルタを突破された。
<div id="mycode" expr="alert('hah!')" style="background:url('java
script:eval(document.all.mycode.expr)')">
さらに次は、ワームの作者がCSRFセキュリティトークンを利用していた。
ワームの作者は、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを手に入れていた。
最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入。
moz-bindingというCSSプロパティは、FirefoxなどのGeckoベースのブラウザではCSS経由でJavaScriptを注入する手段に使われる可能性があることが判明。
###対応策
ブラックリストによる完璧なフィルタは決して作れません。
しかしWebアプリケーションでカスタムCSSを使える機能はめったにないため、これを効果的にフィルタできるホワイトリストCSSフィルタを見つけるのは難しい。
Webアプリケーションの色や画像をカスタマイズできるようにしたいのであれば、ユーザーに色や画像を選ばせ、Webアプリケーションの側でCSSをビルドするようにする。
ユーザーがCSSを直接カスタマイズできるような作りにはしない。
どうしても必要であれば、ホワイトリストベースのCSSフィルタとしてRailsのsanitize()メソッドを使う。
##テキスタイルインジェクション(Textile Injection)
セキュリティ上の理由からHTML以外のテキストフォーマット機能を提供するのであれば、何らかのマークアップ言語を採用し、それをサーバー側でHTMLに変換するようにする。
RedClothはRuby用に開発されたマークアップ言語の一種だが、注意して使わないとXSSに対しても脆弱になる。
###対応策
RedClothは必ずホワイトリストフィルタと組み合わせて使う。
##Ajaxインクジェクション
Ajaxでも、通常のWebアプリケーション開発上で必要となるセキュリティ上の注意と同様の注意が必要。
1つ例外がある。
ページヘの出力は、アクションがビューをレンダリングしない場合であってもエスケープが必要。
in_place_editorプラグインや、ビューをレンダリングする代りに文字列を返すようなアクションを使う場合は、アクションで返される値を確実にエスケープする必要がある。
もしXSSで汚染された文字列が戻り値に含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまう。
入力値はすべてh()メソッドでエスケープする。
##コマンドラインインクジェクション
ユーザーが入力したデータをコマンドラインのオプションに使う場合は十分に注意が必要。
Webアプリケーションが背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド)、syscall(コマンド)、system(コマンド)、そしてバッククォート記法という方法が用意されている。
特に、これらのコマンド全体または一部を入力できる可能性に注意が必要。
ほとんどのシェルでは、コマンドにセミコロン;や垂直バー|を追加して別のコマンドを簡単に結合できてしまう。
対応策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド, パラメータ)メソッドを使うこと。
system("/bin/echo","hello; rm *")
# "hello; rm *"を実行してもファイルは削除されない
##ヘッダーインクジェクション
HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがある。
これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性がある。
HTTPリクエストヘッダで使われているフィールドの中にはReferer、User-Agent (クライアント側ソフトウェア)、Cookieフィールドがありまる。
Responseヘッダーには、たとえばステータスコード、Cookieフィールド、Locationフィールド (リダイレクト先を表す) がある。
これらのフィールド情報はユーザー側から提供されるものであり、さほど手間をかけずに操作できてしまう。
これらのフィールドもエスケープする。
エスケープが必要になるのは、管理画面でUser-Agentヘッダを表示する場合などが考えられる。
さらに、ユーザー入力の一部を取り入れたレスポンスヘッダを生成する場合は、何が行われているのかを正確に把握することが重要。
たとえば、ユーザーを特定のページにリダイレクトしてから元のページに戻したいとする。
このとき、refererフィールドをフォームに導入して、指定のアドレスにリダイレクトしたとする。
redirect_to params[:referer]
このとき、Railsはその文字列をLocationヘッダフィールドに入れて302(リダイレクト)ステータスをブラウザに送信する。
悪意のあるユーザーがこのとき最初に行なうのは、以下のような操作。
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
Rails 2.1.2より前のバージョン(およびRuby)に含まれるバグが原因で、ハッカーが以下のように任意のヘッダを注入できてしまう可能性がある。
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
上のURLにおける%0d%0aは\r\nがURLエンコードされたものであり、RubyにおけるCRLF文字。
2番目の例では2つ目のLocationヘッダーフィールドが1つ目のものを上書きするため、以下のようなHTTPヘッダーが生成される。
HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld
ヘッダーインジェクションにおける攻撃方法とは、ヘッダーにCRLF文字を注入すること。
攻撃者は偽のリダイレクトでどんなことができてしまうのか。
攻撃者は、ユーザーをフィッシングサイトにリダイレクトし(フィッシングサイトの見た目は本物そっくりに作っておきます)、ユーザーを再度ログインさせてそのログイン情報を攻撃者に送信する可能性がある。
あるいは、フィッシングサイトからブラウザのセキュリティホールを経由して邪悪なソフトウェアを注入するかもしれない。
Rails 2.1.2ではredirect_toメソッドのLocationフィールドからこれらの文字をエスケープするようになった。
ユーザー入力を用いて通常以外のヘッダーフィールドを作成する場合には、CRLFのエスケープを必ず自分で実装する。
###レスポンス分割
ヘッダーインジェクションが実行可能になってしまっている場合、レスポンス分割(response splitting)攻撃も同様に実行可能になっている可能性がある。
HTTPのヘッダーブロックの後ろには2つのCRLFが置かれてヘッダーブロックの終了を示し、その後ろに実際のデータ(通常はHTML)が置かれる。
レスポンス分割とは、ヘッダーフィールドに2つのCRLFを注入し、その後ろに邪悪なHTMLを配置するという手法。
このときのレスポンスは以下のようになります。
HTTP/1.1 302 Found [最初は通常の302レスポンス]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:
Content-Type: text/html
HTTP/1.1 200 OK [ここより下は攻撃者によって作成された次の新しいレスポンス]
Content-Type: text/html
<html><font color=red>hey</font></html> [任意の邪悪な入力が
Keep-Alive: timeout=15, max=100 リダイレクト先のページとして表示される]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html
特定の条件下で、この邪悪なHTMLが標的ユーザーのブラウザで表示されることがある。
ただし、おそらくKeep-Alive接続が有効になっていないとこの攻撃は効かない。
多くのブラウザはワンタイム接続を使っているため。
かといって、Keep-Aliveが無効になっていることを当てにするわけにはいかない。
これはいずれにしろ重大なバグであり、ヘッダーインジェクションとレスポンス分割の可能性を排除するため、Railsを2.0.5または2.1.2にアップグレードする必要がある。
#安全ではないクエリ生成
Rackがクエリパラメータを解析(parse)する方法とActive Recordがパラメータを解釈する方法の組み合わせに問題があり、where句がIS NULLのデータベースクエリを本来の意図に反して生成することが可能になってしまう。
(CVE-2012-2660、CVE-2012-2694 および CVE-2013-0155) のセキュリティ問題への対応として、Railsの動作をデフォルトでセキュアにするためにdeep_mungeメソッドが導入された。
以下は、deep_mungeが実行されなかった場合に攻撃者に利用される可能性のある脆弱なコードの例。
unless params[:token].nil?
user = User.find_by_token(params[:token])
user.reset_password!
end
params[:token]が[nil]、[nil, nil, ...]、['foo', nil]のいずれかの場合、nilチェックをパスするにもかかわらず、where句がIS NULLまたはIN ('foo', NULL)になってSQLクエリに追加されてしまう。
Railsをデフォルトでセキュアにするために、deep_mungeメソッドは一部の値をnilに置き換える。
リクエストで送信されたJSONベースのパラメータがどのように見えるかを以下に表示。
JSON Parameters
{ "person": null } { :person => nil }
{ "person": [] } { :person => [] }
{ "person": [null] } { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }
リスクと取扱い上の注意を十分理解している場合に限り、deep_mungeをオフにしてアプリケーションを従来の動作に戻すことができる。
config.action_dispatch.perform_deep_munge = false
#デフォルトのヘッダー
Railsアプリケーションから受け取るすべてのHTTPレスポンスには、以下のセキュリティヘッダーがデフォルトで含まれている。
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
デフォルトのヘッダー設定はconfig/application.rbで変更できる。
config.action_dispatch.default_headers = {
'Header-Name' => 'Header-Value',
'X-Frame-Options' => 'DENY'
}
以下のようにヘッダーを除去することもできる。
config.action_dispatch.default_headers.clear
よく使われるヘッダーのリストを以下に示す。
X-Frame-Options: Railsではデフォルトで'SAMEORIGIN'が指定される。
このヘッダーは、同一ドメインでのフレーミングを許可。
'DENY'を指定するとすべてのフレーミングが不許可になる。
すべてのWebサイトについてフレーミングを許可するには'ALLOWALL'を指定。
X-XSS-Protection: Railsではデフォルトで'1; mode=block'が指定される。
XSS攻撃が検出された場合は、XSS Auditorとブロックページを使う。
XSS Auditorをオフにしたい場合は'0;'を指定します(レスポンスがリクエストパラメータからのスクリプトを含んでいる場合に便利)。
X-Content-Type-Options: 'nosniff'はRailsではデフォルト。
このヘッダーは、ブラウザがファイルのMIMEタイプを推測しないようにする。
X-Content-Security-Policy: このヘッダーは、コンテンツタイプを読み込む元のサイトを制御するための強力なメカニズム。
Access-Control-Allow-Origin: このヘッダーは、同一生成元ポリシーのバイパスとクロスオリジン(cross-origin)リクエストをサイトごとに許可する。
Strict-Transport-Security: このヘッダーは、ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定。
###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 %>
セッションごとにインライン