はじめに
本記事では徳丸本の第4章を元にWebセキュリティの基本を紹介するとともに、
Ruby on Railsの実装例から、人類がどのように脆弱性と向き合ってきたかを紹介します。
XSS
XSSは最もよく発生するWebセキュリティ上の脆弱性として知られています。
(参考)http://it-trend.jp/security_assessment_service/article/market
XSSとは、クライアント側のコンピュータに実行可能な悪意のあるコードが注入されてしまう攻撃手法です。
XSSの種類
反射型XSS
悪意のあるコード(主にJavascript)が、攻撃対象のサイト内ではなく、別のサイトやメールの本文中にある場合を反射型XSSと呼びます。
HTTPリクエスト中にパラメータとして含まれる攻撃コードが、そのまま攻撃対象のWebページ上で動作します。
※以下のようなリンクを踏んでしまうことで発生
http://sample.com/hoge/?moge=<script>alert('a')</script>
持続型XSS
HTTPリクエスト中に攻撃コードが含まれているかどうかに関わらず、対象サイトに訪れた瞬間に悪意のあるコードが実行される場合を持続型XSSと呼びます。
反射型が特定のリンクを踏ませなければいけないのに比して、こちらは攻撃のハードルが低いと言えます。
イメージしやすい例は、掲示板の投稿に悪意のあるコードが含まれていて、その投稿が存在するページにアクセスするだけで、Javascriptが実行されます。
過去には、出稿されたバナー広告の中に悪意のあるコードが埋め込まれていた例もありました。
DOM based XSS
XSSにはサーバーに問題がなくても、クライアント側、特にJavascriptでの表示に脆弱性が発生する場合があります。
本記事ではRailsでのサーバーサイドのWebセキュリティを取り扱うため、対象外となります。
(参考)スクリプトインジェクション入門
XSSへの対策
入力をフィルタし、出力をエスケープすることが具体的な対策となります。
Railsの選択 - 入力
過去のRailsでは以下のようなメソッド群を用いて、不正な文字列を取り除いてきました。
strip_tags(), strip_links()
しかし、ブラックリストのアプローチを元にすると将来必ず漏れが生じるという経験から、ホワイトリストのアプローチを取りました。
https://github.com/rails/rails/blob/e115ace02a88290d2fc707b4979f23728c300950/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb#L72
sanitize()
メソッドを用いることで上記のホワイトリストによるフィルタが可能になります。
Railsの選択 - 出力
「"'<>&」の5つの文字の出力をエスケープすることが対策となります。
escapeHTML()
及びその糖衣構文のh()
メソッドを使うことでエスケープが可能です。
地のrubyでは以下のような記述となります。
require 'cgi'
CGI.escapeHTML('&"<>')
ただし、上記のコードってプログラマーのエスケープ忘れが発生しそうですよね。
そこで現在のRailsでは、erbという標準のテンプレートエンジンを用いることが推奨されています。
以下のコードでは、出力が自動的にエスケープされます。(Rails3以降)
<%= hoge %>
エスケープしたくない場合は以下のように書かなくてはいけません
<%= raw hoge %>
<%= hoge.html_safe %>
SQLインジェクション
XSSの次に被害の多いインジェクション系の脆弱性として、SQLインジェクションがあります。
SQLインジェクションは、SQLの呼び出し方に不備があった場合に発生する脆弱性です。
本来公開すべきでない情報にアクセスできたり、パスワードを用いずに認証をパスしたり、意図しないレコードを追加できます。
※本記事ではXSSとSQLインジェクションのみ紹介しますが、他にもOSコマンドやHTTP,メールヘッダのインジェクションが存在します。
SQLインジェクションへの対策
プレースホルダを利用します。
地のrubyでは以下のコードのような実装になります(※mysql2を利用)
require "mysql2"
client = Mysql2::Client.new(
:host => HOST_NAME,
:username => DB_USER,
:password => DB_PASSWORD,
:database => DATABASE_NAME
)
statement = client.prepare("SELECT * FROM users WHERE name = ? and age = ?")
user = statement.execute(user_name, user_age)
Railsの選択 - ORM
ORM(オブジェクトリレーショナルマッピング)とは、アプリケーションがもつ「オブジェクト」によって、リレーショナルデータベースを操作出来るようにする技法です。
Active RecordはModelの中核をなすRails標準のライブラリですが、ここでORMが実装されています。
Active Recordがもつ機能の一つとして、データを保存する前に検証を行なうというものがあります。
例えばUser.find(1)
というコードから、以下のようなクエリが作成されます。
SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
上記のfindのクエリを作成する記述ですが、下記ソースのような長い検証を経て誕生します。
https://github.com/rails/rails/blob/430a09514a4ad1d0f8481f18a42c4813b6b61e38/activerecord/lib/active_record/relation/finder_methods.rb#L67
注意点
Model.where()
やModel.find_by_column()
のようなメソッドは手動でエスケープする必要があります。
以下の方法でクエリを作成しても、whereメソッド内では、エスケープが行われません。
User.where("age = '#{user_age}'")
以下のようにプレースホルダを利用する必要があります。
User.where("age = ?", user_age)
sanitize_sql()
というメソッドも存在していますが、自分でSQLを組み立てるよりもORMを活用してモデルを作成することがRailです。
CSRF
CSRF(クロスサイトリクエストフォージェリ)とは、Webアプリケーションが本来拒否すべき他サイトからのリクエストを受信し、処理してしまう脆弱性です。
利用者の意図したリクエストであるかを確認せずに処理を受理すると、勝手に書き込みが行われたり、身に覚えがない商品を購入していたといった事例が発生します。
CSRFの対策
対策は以下の2点です。
・CSRF対策が必要なページを区別し、GETとPOSTを適切に使用する。
・意図したリクエストであることを確認するために、GET以外のリクエストに対してセキュリティトークンを追加する。
Railsの選択 - RESTful
RESTfulの設計思想では、処理によってhttpのメソッドを切り分けることが推奨されています。
・データを参照する → GET
・データを新規登録する → POST
・データを更新する → PUT
・データを削除する → DELETE
現在のブラウザで確実にサポートされているメソッドはGETとPOSTだけなので、Railsでは_methodという隠しフィールドを用いてPUTとDELETEを擬似的にサポートしています。
まずはリクエストのHTTPメソッドが何かをしっかり切り分けようというのがRailです。
Railsの選択 - セキュリティトークン
POSTリクエストも (意図に反して) 自動的に送信されることがありえるので、セキュリティトークンが必要になります。
なんと、Railsでは以下の1行でそれが行えます。
protect_from_forgery with: :exception
なおCSRF対策をフレームワークなしで実装する例を知りたい場合は、以下をご参照ください。
とっても簡単なCSRF対策
※徳丸本ではセッションIDをトークンとする方法を推奨していますが、それでは危険という考え方もあるようですね…
セッションとクッキー
上記で解説したXSS攻撃の目的のひとつにcookie(以下クッキー)の入手があります。
クッキー内にセッションidが保存されており、それを盗み出すことが目的ですが、なぜそんなことをするのでしょうか。
まずは簡単にセッションとクッキーとは何かについて紹介します。
セッションとは
HTTPはステートレスなやり取りを基本としています。以前の状態を保持せずそれぞれのやり取りが完全に独立しています。
もしWebの世界がステートレスだったら、ショッピングサイトで画面遷移をする度に、ログインし直す必要が発生します!そんなWebは嫌なので、セッションがあります。
システムにログインしてからログアウトするまでの一連の操作をセッションと呼んでいます。
セッションによってWebの世界は以前の状態を持つ事ができる、ステートフルな世界へ生まれ変わります。
通常セッションは、値のハッシュとセッションidの2つの要素で構成され、Webサーバーのアプリケーション(Rubyなど)が作成します。
セッションidは32文字の文字列で、ハッシュを特定するために使用します。
クッキーとは
クッキーはキーと値の組み合わせになっていて、HTTPレスポンスヘッダを通しWebサーバーからブラウザへ送信され、ブラウザに保存されます。
一方ブラウザは、サーバー毎に個別のcookieを保持していて、アクセスの度にブラウザからWebサーバーに該当サーバーのクッキーを送信しています。
このクッキーの仕組みを利用し、クッキーにセッションidを保存することで、一連のやりとりであることを証明しています。
セッションハイジャック
上記の例から、セッションidとクッキーの組み合わせによって、Webは一時的な認証機能を持っていることが分かります。
つまり、他人のクッキーを奪い取ることができれば、そのユーザーの権限でWebアプリケーションを使うことが出来るわけです。
※ドコモ社のガラケーが2009年夏モデルまでクッキーに対応していなかったそうですが、当時の開発者は一体どのような暮らしをしていたのでしょうか…
Railsの選択 - 無線LANでのセッションハイジャック対策
暗号化されていない通信を避けるため、SSL接続を強制することがRailsでは簡単に出来る仕組みが用意されています。
config.force_ssl = true
セッション固定化攻撃
セッションを盗み出さず、自分のセッションを攻撃対象ユーザーのcookieに保存させるという逆転の発想…そう、それがセッション固定化攻撃です。
攻撃者は一度通常のログインを行なってセッションidをサーバーから受け取ります。
そのセッションidをユーザーのクッキーに保存させてしまうという攻撃手法です。
ですが、別のWebサーバーから対象のWebサーバーのクッキーを書き換えることはできません。同一生成元ポリシーが存在するためです。
どういったケースでこのクッキーの書き換えが発生するかというと、
XSSやHTTPヘッダインジェクションやセッションidをURLに埋め込んでしまうような脆弱性があった場合に限られます。
Railsの選択 - 認証後にセッションidを変更
ログイン成功後に古いセッションを無効にし、 新しいセッションidを発行することが対策となりますが、例のごとく1行で実装できます。そうRailsならね。
reset_session
認証の一連の機能を提供するdeviseなどのgemを使う場合は、コードを書かないで、ログイン/ログアウト時にセッションが自動的に切れます。
セッション管理方法 - Railsユーザーの選択
Railsではセッションの管理方法として幾つかの方法を提示しています。
一つの方法として、セッションストア(セッションの内容の保存先)をActive Recordを用いてDBに保存するパターンがあります。
しかしRails2以降では、セッションストアをクッキーに保存するActionDispatch::Session::CookieStore
という方法がRailになりました。
CookieStoreはセッションidでなく、セッションハッシュを直接クライアント側のクッキーに保存します。
こうすることで、これまでセッションidとその内容(セッションハッシュ)をデータベースに問い合わせて確認する作業が不要となり、パフォーマンスの向上が期待できます。
CookieStoreを利用する場合には以下のような注意点がありますが、詳しくは本記事の対象外となります。
・セッション情報を暗号化する秘密キーの扱いに注意する(特に外部のプロジェクトをクローンしてきた場合など)
・クッキーにいかなる秘匿情報も保持すべきでない
・ブラウザ(ユーザー)がクッキーの値を操作できるため、クッキーの再生攻撃が可能となる。(状態を以前に戻せる)
→そのため、取引情報などをクッキーに保存しては駄目でDBで取り扱う。
※徳丸本では、原則としてクッキーにはセッションidのみを用いることと記述があります。
SessionStoreをRailとしてしまったことで、以下のような脆弱性が発生しました。
http://www.itmedia.co.jp/enterprise/articles/1311/27/news041.html
リダイレクトとファイル/ディレクトリについて
リダイレクト処理にまつわる脆弱性
Webアプリケーションにリダイレクトを行う箇所が存在し、そのURLを外部から指定できる場合に攻撃対象となります。
これはサーバー管理者ではなく、ユーザーのみが被害を被るもので、対策を行うことはWebに関わるエンジニアのマナーとなります。
Railsの選択 - ホワイトリストのURLリスト
ここでもRailsはホワイトリストを用いたURLの照合を推奨しています。
ファイルアップロードにまつわる脆弱性 - ディレクトリ・トラバーサル
ファイル名を外部から指定可能で、そこに相対パスや絶対パスが指定可能な場合に攻撃対象となります。重要なファイルが上書きされるなどの被害があります。
外部からファイル名を指定できる使用を避けるという簡単な解決策もあります。
ファイル名のフィルタリング
[attachment_fuのsanitize_filenameメソッド]
(https://github.com/technoweenie/attachment_fu/blob/fa08cb03914b02b66853b4615cd3eca768291ca7/lib/technoweenie/attachment_fu.rb#L410)の実装を見ていただくと分かりますが、ファイル名のみを取得した後、英数ドットハイフンでない文字をアンダースコアに置換しています。
ファイルダウンロードにまつわる脆弱性
ファイルはアップロードだけでなく、ダウンロードの際も意図しないファイルがダウンロードできないようにしましょう。脆弱性があった場合、秘密キーなどが閲覧出来てしまいます。
対応策は、リクエストされたファイル名が、期待されているディレクトリにあるかどうかをチェックすることです。
※以下参考コードはrailsguidesより引用
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
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'
Railsの知慧
・フィルタリングはホワイトリスト方式を採用
・エスケープすることを標準(Rail)として、エスケープしないことを例外とする
・ORMを通してSQLを安全に組み立てよう
感想
新卒2年目のエンジニア研修として、Webセキュリティの知識整理のために本記事を書きました。
この記事を書くために様々な記事を読みましたが、今の私にはRailsのやってきたこと(特にCoockieStoreやORM)が正解だったのか分かりません。
ただ歴史のあるフレームワークの実装例を学ぶことで、考え(実装の哲学?)を深めるきっかけとしていきたいと思います。
参考
・体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践
・Rails セキュリティガイド
・フレームワークに見る Web セキュリティ対策