はじめに
こんにちは、ガラパゴスのジョンです。 Webアプリケーションを開発するときに、使いやすさや速さや可用性などをよく考慮されますが、今回Webアプリケーションに関わるセキュリティの脅威とその予防方法について考慮しましょう。
特にIoTの導入と共に、キュリティの脅威の範囲がPCとスマホに限らないで、我々の生活の周りのものまで障害を与える恐れがあります。
Webアプリケーションに関わるメインな3つの脅威をそれぞれの特徴とRuby On Rails環境での予防方法をこれから具体的に説明します。しかし、他の環境でも予防方法の基本は同じです。
SQLインジェクション
DBを使うWebアプリがSQLクエリでDBのデータ処理をします。 SQLクエリを生成するにはユーザーからの入力が必要な場合が多いです。 例えば、ログイン画面でE-mailとパスワードを入力し、E-mailで検索するSQL文を生成し、検索結果のパスワードの認証が行うユースケースです。
Ruby On Railsではユーザー検索するためのコードは次のようです。
user = User.where("email = '#{params[email]}'")
ユーザーが特別文字を入力しない場合に次のようなクエリが実行されて、認証操作が正常にします。
SELECT * FROM users WHERE (email = 'hoge@example.com')
しかし、攻撃者がいて、メールアドレスに次のような入力をする場合、
hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('0 hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('1 ...
と実行されるクエリは次のようになります。
SELECT * FROM users WHERE ('hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('0') SELECT * FROM users WHERE ('hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('1') ...
そのクエリの実行結果がUsersテーブルの一番のリコードを取得し、次2番のリコードなどです。その方法で攻撃者が簡単に推測できるパスワードのユーザーアカウントにログインできるまで、繰り返すことができます。
データの盗難の攻撃に留まらず、データの破壊攻撃も次のような可能です。
例えば、ユーザーが自分の作成した記事を削除ボタンを押す時に次のコードが実行されます。
Article.destroy_all("user_id = '#{current_user.id}' AND id = '#{params[:id]}'")
idパラメータに有効な記事IDがある場合に、そのコードが現在ユーザー「current_user」の記事であれば、指定された記事を削除します。
しかし、攻撃者がidに次のデータに挿入する場合、
22' OR 1=1) --
と実行されるクエリは次のようになります。
SELECT "articles".* FROM "articles" WHERE (user_id='2' id = '22' OR 1=1) --')
[OR 1=1] の部分がWHERE文の条件をいつもTRUEにします。また、--部分の後はコメント文になります。 つまり、攻撃者の入力された文で実行されるクエリが他のユーザーの記事を含めて、全ての記事をマッチするので、全てが削除されてしまいます。
それ以外のSQLインジェクション攻撃の方法が様々があります。詳しくは こちらです。
対策方法
基本ルールはユーザーの入力を信じてはいけません。すなわち、入力されたデータをデータとして扱われることの確認が必要です。Ruby On Railsではそうできるために、いろいろな方法が提供されています。次にそれぞれの方法を説明します。
ハッシュの使用
上記の例ではSQL直接を書きましたがそうではなく、パラメッタをハッシュ構造でwhereとdestroy_allなどの関数に渡します。
user = User.where(email: params[email]})
Placeholderの使用
SQL文が複雑な時にハッシュ化が不可になる場合があります。その場合に?記号とパラメッターを使えばSQLインジェクションを防ぐことができます。
Article.destroy_all(["user_id = '?' AND id = '?'", current_user.id, params[:id]])
SantizeまたはQuoteの使用
user = User.where("email = #{User.sanitize(params[email])}") user = User.where("email = #{User.connection.quote(params[email])}")
CSRF攻撃(クロスサイトリクエストフォージェリ)
"Exploit the trust a web server has in browser"
CSRFはWebアプリケーションに存在する脆弱性の一つです。被害者が攻撃用のWebサイトを閲覧することにより、被害者の意図ではない操作が脆弱性のあるWebサイトが受け付けてしまいます。 通常に攻撃Webサイトで送信される要求により状態の変化が行います。例えば、ユーザーの意図せずに商品を注文するまたは、ユーザーの銀行口座から振込するなどの操作です。
現在のブラウザーのセクリティー制約上で、データを取得する要求「GET操作」するだけで、攻撃者がそのデータが見ることができないので、攻撃者にとって意味がありません。 これで、POST操作がCSRF攻撃対象になることはほとんどです。
次に説明するXSS攻撃とCSRF攻撃を一緒に使われたら、攻撃者がGET操作でデータを取得できるので、CSRFとXSS攻撃が両方を同時に使われることはよくあります。
次の例では「onlinebank.example.com」がCSRFの脆弱性のあるオンライン銀行の例です。攻撃者が次のコードを攻撃用のWebサイトに用意し、メールなどで被害者に開くようにリンクを送信します。
被害者がそのページを開くだけで、http://onlinebank.example.com/transfer
へ振込要求が送信されて、銀行のWebサイトがCSRFの脆弱性あるため、ユーザーの意図ではない振込を受け付けてしまいます。
<html><body> <form name="csrf_attack_form" action="http://onlinebank.example.com/transfer" method="POST"> <input type="hidden" name="amount" value="100000"> <input type="hidden" name="transferee_id" value="[attacker_bank_id]"> </form> <script type="text/javascript">document.csrf_attack_form.submit();</script> </body></html>
次の予防方法がCSRF攻撃を起こりにくくしますが、完全に予防しません。
1- Same Origin Policy「同一生成元ポリシー」 Same Origin PolicyとはAJAX要求しているページのロード元とAJAX要求の先が同じスキーム + ドメーンでいなければならないという意味です。 例えば、
http://example.com/dynamic_page
をロードすることにより、AJAX要求が送信される場合に
http://example.com/ajax
に許可されますが、
http://example2.com/ajax
が違うドメインなので、ブラウザのセキュリティ上で許可されません。その方針のおかげでCSRF用のページが攻撃対象のWebアプリケーションにAJAX要求が送れません。
2- X-Frame-Options: SAMEORIGIN AJAX要求と同じように攻撃者のWebサイトより、銀行などの攻撃対象のWebサイトをIFRAMEに表示すること予防するためにHTTPヘッダーに「X-Frame-Options: SAMEORIGIN」送るわけです。
X-Frame-Options: SAMEORIGIN
上記の対策でAJAX、IFRAMEでCSRF攻撃をできなくなりましたが、フォームの送信で攻撃は可能なので次のように対策方法を説明します。
対策方法
POST、PUT、PATCH要求を受け付ける時に攻撃者が推測できないトークンを用います。そのトークンを申請フォームなどのフィールドとして、追加します。要求を受け付ける時にトークンの認証が行って正しくなければ、要求を拒否します。Same Origin Policyのおかげで、攻撃者が攻撃対象のWebサイトにより、ユーザー側に保存されたクッキーを見れませんので、CSRFトークン認証することでその攻撃を予防できます。
Ruby On RailsではCSRF攻撃の対策がコア機能に組み込まれているので、各コントローラーに次の行を追加するだけでCSRFの攻撃予防ができます。
しかし、GET要求がCSRF攻撃に狙われる価値が普段にないので、CSRF攻撃予防の対象になっていません。つまり、Webアプリケーションの設計ルールとして、GET要求で状態変更する機能を作ることは避けるべきです。 また、XSSの脆弱性があれば、CSRF攻撃予防を回避することは可能になってしまう場合があります。
class ApplicationController < ActionController::Base protect_from_forgery end
XSS攻撃(クロスサイトスクリプティング)
"Exploit the trust a browser has in a legitimate website"
XSSとは信頼性のあるWebサイトに悪意のあるコードを埋め込む攻撃方法です。 XSS攻撃の書類が3つあります。
タイプ1:Reflected XSS攻撃
埋め込まれるコードが直接URLのパラメータとして提供される場合にReflected XSS「反映的なXSS」と言います。
次のデモ用のURLが検索のWebアプレケーションです。queryのパラメータに検索キーワードを入力すれば検索してくれて結果がない場合に、"No result found for [入力したケーワード]"が表示されます。
ページの内容は次のようになります。
<div> Sorry, no results were found for <b>Hello World</b>. ... </div>
しかし、攻撃者が悪性のコードを用意し、queryパラメータに埋め込んで、作成したURLを被害者をクリックさせる場合、
queryパラメータに適切な検証せずにそのまま、ページに埋め込んでしまいした。その結果、攻撃者が用意したJAVASCRIPTコードが被害者のブラウザーに送信されて、実行されてしまいます。
被害者のブライザーに送信されるページの中身は次のようです。
<div> Sorry, no results were found for <b><script>alert('Reflected XSS')</script></b>. ... </div>
タイプ2:Persistent / Stored XSS攻撃
悪性のあるコードをコメントなどで入力し、データベースに保存されます。これで、被害者が信頼しているページを見るときに攻撃者の入力が表そのまま、ブラウザーに送信されることで悪性のあるコードが実行されてしまいます。
次の例では「Persistent / Stored XSS攻撃」を試すことができます。"Google Chromeのみに動きます"
下記のURLではコメントを書く簡単なWebアプリケーションです。
https://xss-doc.appspot.com/demo/1
例えば、
Hello Guys
を書くとコメントの一覧に表示されます。
しかし、ユーザー入力をヴァリエーションせずにコメントをそのまま表示しているため、XSS攻撃の脆弱性があります。
Javascriptのコードをコメントに入力すると、今度そのページを見る人のブラウザーで、入力したコードが実行されてしまいます。
コードのバリエーションがいっぱいあるため、すべてを予防することは簡単ではありません。次はコードのバリエーションの一部です。
<iframe src="data:text/html;base64,PHNjcmlwdD4NCmFsZXJ0KCJYU1MiKQ0KPC9zY3JpcHQ+" />
<img src="http://example.com/nonexistent.png" onerror="jAvascript:alert('Persistent XSS');" />
<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg">
Samy Worm
Myspaceが特定なHTMLタグの入力を自分のプロフィールに追加することを許可しましたが、SCRIPTタグ、EVENT属性「onclick」、「onload」、「onなんでも」などを積極的にフィルターしました。 しかし、「Samy Kamkar」さんがJAVASCRIPTコードをSTYLEタグに埋め込んで、自己複製のWormを工夫しました。そのWormを自分のプロフィールに埋め込んだら、20時間以内に1,000,000人がそのWormを実行してしまいました。 技術的な明細情報はこちらです。
「Persistent / Stored XSS攻撃」と「Reflected XSS攻撃」が悪性のあるコードがサーバー型で処理されて、ユーザーのブラウザーに漏れるため、サーバー側の脆弱性と見なしますが、クライント側でもXSS攻撃があります。
タイプ0:DOM Based XSS攻撃
その一方、サーバー側で脆弱性ではなく、クライント側で実行されるコードの脆弱性でパラメッターなどの処理によって、DOMに悪性のあるコードが埋め込まれて、実行されてしまうことはDOM Based XSS攻撃だと言います。
例えば、次の例が3つの画像の一つを表示しています。#記号の後の番号で、現在表示している画像を決まります。その操作はJAVASCRIPTコードで実装されています。
画像1表示するには https://xss-doc.appspot.com/demo/3#1
画像2表示するには https://xss-doc.appspot.com/demo/3#2
などです。
しかし、次のURLを開くと、URLに埋め込まれたJavascriptコードが実行されてしまいます。
#記号の後の部分はサーバーに送られないため、サーバーが悪性あるコードを検出することはできません。
XSS攻撃の予防方法
XSSのタイプによって、対応方法が異なりますが、こちらは一般なガイドラインです。
- ユーザーからのパラメータを表示する前に絶対エンコードします。Ruby On Railsではブラウザーに送信されるデータが自動的にエンコードされてもらいますが、raw または .html_safe を使えば、HTMLエンコードを無効にできます。両方のメソッドを使う時に注意が必要です。
"<script>alert('XSS')</script>".html_safe raw "<script>alert('XSS')</script>"
特定なタグを許可し、それ以外のタグを削除したければ、sanitizeメソッドを使えばいいです。
sanitize("<p>safe code</p><script>alert('unsafe code')")</script>
結果は
<p>safe code</p>
がブラウザーに送信されます。
Ruby On Railsでコードのフィルタリングのメソッドがいろいろあります。詳しくはこちらです。
また、文字列をJAVASCRIPTに埋め込む際に「escape_javascript」メソッドを使用すべきです。
<script> var x = "<%= escape_javascript(unsafe_data) %>"; ... </script>
- ユーザーからのパラメータをバリデーションします。
- HTMLコードをユーザーから拒否し、マークダウンなどのセーフな記法を使用します。
- Ruby On Railsでは悪性のあるコードがDBに存在するかどうかをスキャンしてくれるGemはこちらです。
- X-XSS-Protectionヘッダーの追加: タイプ1(Reflected XSS攻撃)を予防するために「XSS Auditor」という機能が現在のブラウザに埋め込まれています。サーバーからのHTTPレスポンスにX-XSS-Protectionヘッダを指定することで、その機能を有効にできます。
X-XSS-Protection: 1; mode=block
参考文献
https://www.coursera.org/learn/software-security
http://chris.vandenberghe.org/publications/csse_raid2005.pdf
Ruby on Rails Security Guide — Ruby on Rails Guides
Cross-Site Request Forgery (CSRF) - OWASP
Types of Cross-Site Scripting - OWASP
https://www.owasp.org/index.php/Ruby_on_Rails_Cheatsheet
https://www.ipa.go.jp/files/000024729.pdf