セキュキャン 2015 高レイヤートラック(Jxck)
本資料は、セキュキャン 2015 高レイヤートラックの講義資料です。
セキュキャン参加者であるセキュリティエンジニアの卵を対象に、 Web のセキュリティの知見が、実際どのように Web アプリ開発に反映されているか、もしくはどう反映すべきかを、フレームワークの視点から解説することを目的としています。
将来、 Web のセキュリティに興味を持ったエンジニアが、その知見を多くの開発者に啓蒙する手段として、フレームワークに反映するというのは非常に有効な方法です。
ここではその実例として Rails を例にとり、 Rails がこれまでに積み上げてきたセキュリティに関する知見を振り返るとともに、フレームワークとしてそれをどう取り入れているかを解説します。
フレームワークに見る Web セキュリティ対策
Intro
Web アプリケーションを開発する場合、 Web アプリケーションフレームワーク(以下フレームワーク)を導入する場合が多い。
フレームワークは、 主に開発効率の向上を目的として、 Web アプリケーションを開発する際に頻出する共通処理の抽象化と汎用化を行うが、同時にこの抽象化された部分に、セキュリティ対策を盛り込むことが可能な場合がある。
特に、多くのユーザのいる OSS のフレームワークには、脆弱性に対する知見が集まりやすいため、フレームワーク自体に対策が施される場合がある。
こうして知識が集積されたフレームワークは、既知の問題に対策するための補助機能が提供されている場合が多く、特にそれを意識せずに開発しても、フレームワークを正しく使うだけである程度の安全性が確保できる場合がある。
したがって、利用者が多く、開発が活発で、ある程度成熟しており、パッチの公開が迅速で、ノウハウや、補助的なライブラリが多く公開されている、いわゆる「人気のあるフレームワーク」を選択すること自体が、セキュリティ対策の一つのアプローチとして無視できないものであると筆者は考える。
Rails とエコシステム
Rails はサーバサイドのフレームワークとして、非常にユーザの多いプロダクトの一つである。
Rails は、フレームワーク自体が単に機能を提供するだけでなく、Web アプリを開発する上での指針を示している。これは レール と呼ばれ、 Rails が敷いているレールにきちんと沿った開発をすることは、 Rails を使いこなす上では非常に重要になる。
またこのレールは、フレームワーク自体の拡張にも適用できるため、 Rails 本体に不足する機能を補うライブラリやプラグイン、ツールなども、このレールに合わせてコミュニティで多く作られている。
一部のものは、 Rails のアップデートで本体に取り込まれることもあり、そのプロセスの一環でセキュリティに関する対策も強化される場合がある。
こうしたコミュニティが一体となったエコシステムのおかげで、 Rails は非常に成熟したフレームワークの一つと言えると、筆者は考えている。
Rails のセキュリティ対策
登場からこれまでの 10 年近い歴史の中で、 Rails が「セキュリティ」とどう向き合ってきたのかを知ることは、現在の Web の基本的なセキュリティ対策の知見を得る上で有効な手段の一つと言える。
レールに則った開発を行うことで、 Rails が蓄積してきたセキュリティ対策の恩恵を受けることができ、 Web 開発に頻出する脆弱性に対して、最低限安全なアプリケーションを開発することができる。
また、これを促進するため、 Rails は公式にセキュリティに関するガイドを公開しており、 Rails を用いた開発を行う場合には必読である。
Ruby on Rails Security Guide (訳)
これはあくまで Rails の使い方としてのガイドとなっている。しかし、本資料の目的は、 Rails 自体の使い方の紹介ではない。
本資料の目的は、 Rails に蓄積された知見から、 Web のサーバサイドの開発において知っておくべき知識と対応方法を抜き出し、 Rails 以外の Web 開発一般に応用できるようにまとめることを目的とする。
フレームワークを用いた開発や、フレームワークを選定する際の参考にすべき視点と共に、フレームワークという立場がセキュリティについてどういう態度を取るべきか、フレームワーク自体を開発する場合どうするべきか、セキュリティの知見をフレームワークに反映する場合どういう方針がありえるか、という視点でも参考になることを願う。
XSS 対策(html escape)
Web アプリでの HTML 出力時には、 HTML Special Character をエスケープするのが鉄則と言える。
ユーザからの入力は生のまま保存し、 View 層で HTML に埋め込む際にエスケープするのが基本であるため、通常エスケープはテンプレートエンジンなどで行われる。
ユーザはテンプレートエンジンの機能を適切に使い、自分で言語の文字列結合演算子などを安易につかった処理をしないようにするのが鉄則である。
Rails での対策
Rails はデフォルトでは erb というテンプレートエンジンが使われ、テンプレートに渡した Ruby の変数が埋め込まれるが、 Rails 2 まではエスケープは明示的に行う必要があった。
# before
<%= foo %> # raw
<%= h foo %> # escaped
Rails 3 からは、エスケープの方がデフォルトになり、エスケープしない場合は明示的に行うように変更された。
# after
<%= raw foo %> # raw
<%= foo %> # escaped
エスケープする文字列は以下。
# *1
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' }
対策方針
フレームワークとしては、 HTML エスケープの機能を持つのは必須といえ、デフォルトをどちらにするかという判断が必要になる。
判断基準は、デフォルトを (する | しない) のどちらにするかと、埋め込む値が (すべき値 | しない値) のどちらかにより、この組み合わせを開発者が間違えた場合にどうなるかに着目する。
明示的にやらなくてはいけない場合は、かならず「やり忘れ」を考慮する必要があるため、注目すべき組み合わせは大きく2つある
- エスケープしたくない値を、エスケープしてしまった (表示崩れ)
- エスケープすべき値を、エスケープし忘れた (XSS)
誤って HTML タグがそのまま表示され、崩れてしまうことにより、ユーザに不便をかけるのも十分問題だが、安全側に倒す fail safe の視点に立てば、たとえ表示が崩れてしまったとしても、全ての文字列をエスケープで表示する方が、エスケープ漏れにより XSS が仕込まれるよりは良いと考えられる。
Rails 的に言えば、「エスケープを必ずする」というのがレールであり、「エスケープを迂回する」のはレールを外れることを意味することになる。レールに乗っていれば間違いは少なくなり、レールを外れる場合(ここでいう、明示的にエスケープを外す場合)は、開発者に緊張感を与え、テストやレビューなどによる検証を促す意味で効果がある。
補足
自動で全てエスケープするという処理だけだと、細かい制御をしたい場合に使いにくい場合もある。
例えば、ユーザの入力から複雑な HTML などを動的に組み立てるといった場合などである。
# ruby 内で HTML を組み立てる
foo = 'foo'
link = "<a href='#{foo}'>#{foo}</a>"
# raw でないとリンクにならない
<%= raw link %>
こうした文字列結合による組み立てをユーザが行っては意味がないため、 Rails では基本的な HTML は全て、安全に組み立てられるヘルパを用意している。
これにより、自分で文字列連結を行うこと自体が、レールから外れる行為としている。
link = link_to foo, foo
全てのケースがカバーできるわけではないので、必要に応じて自分でヘルパを作る場合のために、エスケープを行う関数はもちろん別途提供されている。
ユーザが変換すべき記号について気にする必要はない。
また、複数のヘルパが組み合わさるような場合に、複数のエスケープが実施されてしまうケースが考えられるが、 Rails はこの問題を回避するため「その値がエスケープ済みか」(HTML に展開しても安全か)という情報を取る手段を持っている。
foo.html_safe?
自分で HTML を文字列として組み立てるような場合や、エスケープする文字を強く意識するような場合は、何かがおかしいと意識する必要がある。
フレームワークは、そうならないためのツールや指針を提供するべきである。
link
- https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/string/output_safety.rb#L6
- http://www.slideshare.net/heikowebers/ruby-on-rails-security-updated-rails-3-at-railswaycon
SQLInjection 対策(sanitize_sql())
HTML のエスケープ同様、 SQL も発行直前にエスケープするのが鉄則である。
ユーザからの入力を where 句のパラメータなどにして SQL の組み立てを行うような場合は、プレースホルダ(一般的に ?
マーク)を用いるが、こうした機能は ORM が提供しているためそれを用いる。
ユーザは ORM の機能を適切に使い、自分で言語の文字列結合演算子などを安易につかった処理をしないようにするのが鉄則である。
Rails での対策
Rails では、 ID を用いた取得や、 where 句の単純な指定による取得など、基本的な処理についてはヘルパメソッドが提供されている。
User.all # select * from users
User.find(10) # select * from users where id = 10
User.find_by(name: "Jxck") # select * from users where name = 'Jxck'
User.where(name: "Jxck") # select * from users where name = 'Jxck'
User.select("address, email") # select address, email from users
User.limit(10).offset(20) # select * from users limit 5 offeset 20
こうしたメソッドを使うのが基本で、この場合は ORM である ActiveRecord が SQL 文字列の組み立て時に SQL エスケープを行ってくれる。
ユーザの入力を元にする場合も、値をハッシュで渡してやればよく、基本的に自分で SQL を文字列連結する必要はない。
しかし、複雑なクエリを発行する場合や、パフォーマンスチューニングなどの理由で、自分で組み立てたクエリを発行したい場合もある。
この場合プレースホルダなどを用いて値を埋め込むことになる。
# Good
User.where("name = ?", name)
User.find_by_sql("select * from users where name = ?", name)
まちがっても、自分で文字列演算(展開)などによってクエリを組み立ててはならない。
# Bad
User.where("name = '#{name}'")
User.find_by_sql("select * from users where name = '#{name}'")
Ruby は、柔軟なメタプログラミングが可能なため、 DSL が作りやすく、大抵の SQL は ActiveRecord のメソッドレベルで記述することができる。
そのため Rails では SQL を自分で書くということ自体が、レールから若干外れており、気をつけるべき処理である。
そして、それらメソッドを用いた記述の方が、自分で文字列を組み立てるよりも読みやすいコードになりやすいため、それ自体が SQLInjection の脆弱性を作りこみにくくしている。
一般的に、安全でない方法は、安全である方法よりもコストが高くなるように設計しておくベキである。
対策方針
Ruby のような柔軟な表現が可能な言語では、 ActiveRecord のように DSL インタフェースを提供することで、プログラム自体の見た目が SQL に近くなるように設計できる場合があるが、全ての言語で同様のことが可能とは限らない。
また、静的なインタフェースとして SQL 標準の構文を提供したとしても、各 RDB によって SQL に独自の方言があるので、結局文字列ベースの SQL を完全に排除するのは難しいのが実際である。
したがって、ベースとしてはプレースホルダ方式のインタフェースを提供し、プレースホルダに変数を展開する箇所で SQL エスケープを行う。
利用側としては、絶対に値の文字列結合による SQL を作らないということになる。
link
- http://rails-sqli.org/
- http://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql
- http://blog.tokumaru.org/2013/12/sql.html
CSRF 対策(protect_from_forgery)
CSRF 対策は、ワンタイムトークンを使う方法がデファクトとなりつつある。
具体的には、対象の画面にワンタイムトークンを入れておき、リクエストにそれが入っているかを確認することで、意図した画面のフォームから行われたリクエストかを識別する方法である。
Rails での対策
Rails ではデフォルトでこの対応が入っている。
Controller で protect_from_forgery
を有効にするメソッドを呼び出す。(デフォルトで有効)
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
Rails では form_for
という View ヘルパでフォームの HTML を生成する。
このとき、 authenticity_token
という hidden パラメータにワンタイムトークンが仕込まれる。(Rails 2 までは固定値、Rails 3 からはランダム)
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓" />
<input type="hidden" name="authenticity_token" value="48mHRVY8HC37NPF1==" />
<div class="field">
<label for="user_name">Name</label><br>
<input type="text" name="user[name]" id="user_name" />
</div>
<div class="field">
<label for="user_pass">Pass</label><br>
<input type="text" name="user[pass]" id="user_pass" />
</div>
<div class="field">
<label for="user_mail">Mail</label><br>
<input type="text" name="user[mail]" id="user_mail" />
</div>
<div class="actions">
<input type="submit" name="commit" value="Create User" />
</div>
</form>
Form の submit 時にこの値がサーバに送られるため、サーバ側でセッションに保存したトークンとリクエスト内のトークンが一緒であるかを確認する。
一緒であれば、この画面のこのフォームからのリクエストであることがわかる。
また、 Ajax リクエストを発行する場合は、 hidden ではなく meta タグに仕込まれた値が使われる。
これは csrf_meta_tags()
を用いて、トークンを埋めた meta タグを View に出力している。
Rails では Ajax 系の操作は jQuery ベースのヘルパライブラリが提供されており、それを経由して行う場合が多い。
このライブラリは meta タグに仕込まれたトークンを X-CSRF-Token
ヘッダに追加するためサーバはそちらを使って検証する。
<meta name="csrf-token" content="ASXsUgCoNQsVHPH7/BooqI259/lNoCh3kwrtxSF/0F1vK66ziLMBwG48hmOWJbYiRe1OVsamY9zJ+Bg/7uxBTQ==" />
対策方針
この方式の対策を行うためには、通常 View と Controller 2つに手を加える必要があるため、フレームワークの構成によってアプローチは少し変わる。
特に以下の3つは暗黙的に行わないと、「やり忘れ」による漏れを防ぐのが難しい。
- HTMLにトークンを仕込む
- セッションにトークンを保存する
- レスポンスにトークンが含まれているかを検査する
Rails はユーザよりも先にリクエストに共通処理を行うミドルウェアがあるため、トークンの確認とエラーの送信もほぼ自動で行うことができる。
こうしたミドルウェアの層は、最近のフレームワークはだいたい備えているので似た様な方針が取れる。
また View でのフォームの組み立ては基本的にヘルパを使うというレールがあったため、 HTML への埋め込みが容易であるが、フレームワークによっては、開発者がトークンを View に明示的に渡す必要があるものもある。
この辺は、フレームワーク自身と View の結合度合いにもよるが、できるだけ暗黙的に行われていることが望ましい。
また、最近ではフォームを submit するのではなく、 Ajax でリクエストを投げる場合も多い。
Ajax は、そもそもフォームが無い場合と、ページが更新されない場合があるので、トークンの置き場所と、その期限・更新方法について考える必要がある。
補足
View のキャッシュ機構を持つフレームワークの場合、 CSRF 対策のトークンがキャッシュされると他人のトークンがわたってしまう場合があり得る。
Django という Python フレームワークは、かつてこの種のバグがあった。
link
- http://takagi-hiromitsu.jp/diary/20050427.html
- http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html
Mass Assignment 対策(strong parameter)
Web に限らず、ユーザからの入力を用いて処理をする場合は、その入力について最低限のバリデーションを行い、想定した値であるかを確認するのが鉄則である。
一般的な Validation (型やフォーマット)の確認についての機能は Rails にもあるが、ここでは値の存在確認について解説する。
例えば以下のようなフォームが有ったとする。
<form action="/users" method="post" .....>
<input name="user[name]" ... />
<input name="user[password]" ... />
<input name="user[email]" ... />
<input type="submit" ... />
</form>
submit の結果、以下のようなリクエストボディが POST でサーバに送られたとする。
user[name]=Jxck
&user[password]=pass
&user[email]=jxck@example.com
Rails はこのボディを params
という変数に、 Ruby の hash の形に変換して格納する。
以下のコードは、その値をそのまま User.create()
メソッドに渡しており、裏では insert 文が発行されることになる。
# Controller
class Users < ApplicationController
def create
# params はリクエストボディをオブジェクトにしたもの
# params[:user] = {:name => Jxck, :password => pass, :email => jxck@example.com}
@user = User.create params[:user]
end
end
params
に格納された入力をそのまま渡しているため、例えば HTTP のボディに値を追加することで、 DB のテーブルに存在する列であれば好きに保存できる可能性がある。
user[name]=Jxck
&user[password]=pass
&user[email]=jxck@example.com
&user[is_admin]=true
問題はこの一行である。
User.create params[:user]
このように、短く書けることを優先したあまり、その中身についてきちんと気を配っていないことが問題であり、同様のことは他の言語のフレームワークでも起こりえる。
Rails の場合は特に、 Scaffold というひな形生成ツールがこうしたコードを生成していたため、同じように書いた開発者が多かったという問題もある。
Rails での対策
Rails 4 では、これを解決するために Strong Parameter という機能をコアに取り込むことになった。
Strong Parameter は Controller の層で入力を明示的に検査し、必要な値だけを Model に渡すことができる。
# Controller
class Users < ApplicationController
def create
# permit でホワイトリスト指定
attr = params.require(:user).permit(:name, :password, :email)
@user = User.create attr
end
end
ただし、それ以前も対策方法がなかったわけではない。
まず、以下のように Model に、更新対象をホワイト/ブラックリストで指定することができる。
class User < ActiveRecord::Base
attr_protected :is_admin # ブラックリストの例
end
上の例のようなブラックリストによる対処の場合は、カラムが増えたときに更新などによる検査漏れが発生する可能性がある。
しかし、これはあくまで MVC でいう Model への定義になるが、実際には画面によって入力項目は違い、 更新可能かどうかは URL によっても変わる。
したがって、一概に Model でのホワイトリストが可能とは限らず、それをやるためには同種のテーブルに対する別のモデルなどがありえるが、多少 Rails のレールから外れる設計になる。
Rails 3.1 では、role というものを定義し、 Controller でその role を選択する方式を一旦採用した。
# Model
class User < ActiveRecord::Base
attr_accessible :name
attr_accessible :name, :email, as: :name_and_email
end
# Controller
class Users < ApplicationController
def create
@user = User.create(params[:user], role: 'name_and_email')
end
end
しかし、これでは role が増えすぎる問題があった。
結果、 Controller だけで完結する指定方法として前述の Strong Parameter を採用することとなった。
対策方針
Mass Assignment は、どんなフレームワークを使っていても起こりえる問題である。
根本的には、ユーザ入力や HTTP リクエストの値を何も検査せずにロジックに回していることが問題と言える。
近年のフレームワークは「綺麗にスッキリ書く」ことを重視する傾向にあるため、ともすれば過度な省略によって、明示的に指定すべきものが抜け落ちている場面も体感としては少なくない。
抽象化はメリットがあるが、それによって隠された部分に無頓着では、安全なアプリは作れない。
Rails に問題があるとすれば、それをより低いコストで防ぐ仕組みがなかったことと言えるだろう。
フレームワーク側も、頻出する問題があるならば、適切な負荷で達成できる方法を提供するのが望ましい。
また、対策を実施する場合は、その対策の責務をきちんと判断して手段を提供する必要がある。
実際に保存される値に関する問題であれば Model であるが、今回の本質的問題は「入力値を受け取った時点」であると考えると、対策は Controller で行うのが筋が良い対策と言える。
補足
ただし、 Controller で対策を行うのが筋の良い対策であるという結論に至るまでにも、多くの試行錯誤があった。
特にこの問題については、入力確認がおろそかであるため、任意の値でデータを更新できる脆弱性を持ったアプリが多く存在したという問題がある。
その問題を ある開発者が issue で指摘した が、それが相手にされなかったため、
Github が実際に抱えている同種の脆弱性 を突いて見せたところ、
Github がその開発者のアカウントを一時凍結したという事件があり、話題になった。
ちなみに、このとき脆弱性をついて以下のような操作が行われたという。
- 投稿時間を未来にする
- 脆弱性を用いて Rails のプロジェクトに自分の公開鍵を登録し、リポジトリにファイルをコミットする。
これがきっかけになり、 Mass Assignment の対策が検討され、試行錯誤の結果、現在の実装に至る。
link
- https://github.com/rails/rails/issues/5228
- https://github.com/blog/1068-public-key-security-vulnerability-and-mitigation
- http://blog.sorah.jp/2012/03/05/mass-assignment-vulnerability-in-github
Session Fixation 対策(reset_session)
ログイン前の Cookie をログイン後もそのまま使うと、ログイン前の Cookie が Session Fixation によって仕込まれたものである場合、セッションが乗っ取られる可能性がある。
Session Fixation 攻撃を防ぐ方法として、ログイン成功時にセッション用の Cookie を更新するという手段がよくとられる。
Rails での対策
これは該当の Controller の最初に一行追加するだけで対策できる。
reset_session
対策方針
セッションの更新は、ミドルウェアの層で行うことができるため、一般的なフレームワークであれば対応方法はそこまで変わらないだろう。
更新すべき場所が複数箇所ある構成の場合には、漏らさずやる必要がある点は注意が必要ではある。
これは Rails の場合、開発者が漏れ無くやる必要があるという点で足りてないと言える部分だが、普通はそこまで多くないので問題にならないと考えられる。
また、セッションの途中、例えば買い物をしていて、支払いの直前にログインする場合などは、そこまでの買い物かごの情報が消えないように工夫が必要となる。
この場合は、 Cookie の文字列だけ変更し、サーバ側に保存されたオブジェクトを付け替える必要がある。
既に、乗っ取られている可能性を考慮するのであれば、ログイン時の User-Agent や IP を保存しておき、それが変化していないかを調べる手段を提供できるとより良い場合もあるが、ミドルウェアの範囲で実装が可能であり、ライブラリとして提供する範囲と考える。(Rails も標準ではサポートしていない)
link
- http://railsdoc.com/references/reset_session
- http://api.rubyonrails.org/classes/ActionController/Base.html
パスワード漏洩対策(has_secure_password)
パスワードを平文で保存する場合、 SQLInjection や DB アカウントの乗っ取り、 DB をハードごと盗まれる、などにより、パスワードそのものが流出するリスクがある。
Web アプリの場合は、 OAuth などを用いて自前ではパスワードを持たないという方針も最近ではよくとられるが、そうした手法が取れない場合や OAuth のプロバイダ側になるサービスなどはパスワードを預かる必要がある。
ユーザにパスワードを登録させる場合、 DB には平文を登録する代わりに、ハッシュ化した値を保存することで、流出時のリスクを低減させるという手法が取られることが多い。
(パスワードリマインダが実装できなくなるが、代わりにパスワードリセットを実装するのが主流となっている)
ハッシュを保存する場合も、単純なハッシュでは DB 自体が盗まれるような場合はレインボーテーブルによる解析で復元される可能性もゼロではない。
したがって、要素としては、大きく以下を考慮する必要がある。
- ハッシュアルゴリズム
- stretching
- salt の有無
Rails での対策
Rails は 3.1 から、ユーザ登録などを想定したパスワードとその再入力値の検証から、パスワードをハッシュ化し保存するまでをサポートしている。
ハッシュには、最初 salt を用いた SHA2 だったが、すぐに bcrypt に変更されている。
(現時点では bcrypt の実装自体は外部ライブラリ(gem) を入れる必要がある)
具体的には、以下のようにモデルに対して has_secure_password
を有効にする。
class User < ActiveRecord::Base
has_secure_password
end
これにより、モデルには password
と password_confirmation
が定義される。
ただし、実際に DB に保存されるのは、 password_digest
というハッシュ値だけである。
一般的なユーザ登録フォームのように、モデルで登録したパスワードとパスワード確認の入力を求める HTML を作成する。
非常に簡略化したテンプレートは以下のようになる。
<%= form_for @user do |f| %>
<div class="field">
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.password_field :password_confirmation %>
</div>
<div class="actions"><%= f.submit %></div>
<% end %>
コントローラでは、ユーザオブジェクトを生成する際にこれらの値を渡す。
必要な操作は以下のようになる。
user = User.new(:name => "Jxck", :password => "a2luZG9mbmljZXBhc3N3b3Jk", :password_confirmation => "a2luZG9mbmljZXBhc3N3b3Jk")
これで自動的に password
と password_cinfirmation
の同一性チェックが行われ、 digest が生成・保存される。
ログインは時は、パスワードで認証を行う。
user = User.find_by_name("Jxck")
user.authenticate("a2luZG9mbmljZXBhc3N3b3Jk")
開発者にとっては、基本的に必要な実装は has_secure_password
というメソッドの呼び出しだけであり、これにハッシュの生成や、それを用いた検証が抽象化されており、実際の digest 値すら意識する必要が無い。
ここまで簡単になると、あえて平文パスワードを保存する実装の方がコストが高くなる可能性すらあり、レールに沿った実装を行えば、ある程度の安全性を確保したパスワード認証が、比較的低いコストで実現できることになる。
また、こうしてレールが引かれたことにより、何か拡張を必要とする場合は、このレールに準じた拡張を行うライブラリを提供しやすくなる(Rails 自体に頼れる部分も増える)。
対策方針
フレームワークがサポートすべき範囲としてはいくつか考えられる。
- 何も提供しない
- ハッシュ、 salt、 stretching などのデフォルトを提供する
- パスワード認証のスタンダードなフローを提供する
このレイヤは考慮すべきことが非常に多いため、個人的にはサポートは必須では無いと考えている。
フレームワークではなく、要件に合ったライブラリが選択されることも多いだろう。
しかし、もし提供する場合は選択肢が多くて開発者が悩み安い部分について、指針を提供するというのが一つとしてある。
具体的には、数あるハッシュ関数から、妥当なものと適切な値を標準として提供するというものである。
それにより、ハッシュ関数についての知識が少ないエンジニが、弱いハッシュを選んでしまうことを防ぐことができるからだ。
bcrypt は単なるハッシュ関数ではなく、そのプロセス中に salt の生成と、 stretching が含まれている。
したがって、前述の三つの要素が bcrypt を選ぶことで自動的に決定できる。
require 'bcrypt'
my_password = BCrypt::Password.create("my password")
# $2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa
この場合、生成されたハッシュはこう読む。
-
$2a
:/etc/shadow/
でのハッシュ方式の識別子 -
$10
: stretching の回数 -
vI8aWBnW3fID.ZQ4/zo1G.
: salt (22 文字) -
q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa
: digest
つまり、生成されたハッシュ自身に salt や stretching 回数などの情報が含まれているため、別途 DB にそれらの値を格納する必要が無くなり、実装がよりシンプルになる。
また、 SHA ではなく bcrypt を用いる理由のもう一つとして計算コストがある。
通常計算コストは低いほど良いとされるが、パスワードハッシュの計算コストが低いということは、 DB 自体が盗まれた場合にレインボーテーブルアタックなどの攻撃も高速に行えることを意味する。
そこで、 bcrypt はあえて計算コストを高くすることで、非現実的な攻撃時間にし、リスクを低減させる方針を取る。
もちろん、このコストが高くなるとログインなどでユーザを待たせることにも繋がる。また開発の側面で言えばテストの実施時間ものびる。
bcrypt の gem は、このコストをある程度調整可能になっているため、必要に応じて調整することになる。
ただし、ハッシュ関数については、 bcrypt のより良い実装の登場や、別のハッシュ方式が出てきた場合、既存の実装に手を加える必要が最小限に押さえられるように差し替え可能にしておくことが望ましい。
さらにその上に汎用的な認証を実装する場合、求められる要件の幅も広いため、実装が複雑になりがちだと言う問題があり、標準で提供しないフレームワークも多い。
Rails にしても、いくつかのライブラリが提供されながら、(よく使われるものの移り変りが激しいという意味も含め)決定版が無く、また扱うのが難しいものが多かった。
一方で、自分でパスワード認証を実装するには、工数もそれなりにかかり、バグがあれば漏洩につながりやすいため、コストが大きかった。
(Rails にはかつて、そのチュートリアルがあったが、それなりのページ数が割かれていた)
個人的には、ここをフレームワーク自体が提供することは必須では無いと考えている。
has_secure_password
は、もちろん全てのケースをカバーする訳ではないが、一番スタンダードと呼べる方式について、レール(この場合、 password
と password_confirmation
という名前のプロパティを用意すれば、 bcrypt でハッシュ化し、 authenticate()
というメソッドで認証できるという流れ)を用意したという点が評価できる。
link
- http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html
- http://railstutorial.jp/chapters/modeling-users?version=4.0#sec-an_encrypted_password
ハッシュ化自体の議論も多い
- http://blog.jmuk.org/2013/02/blog-post_4.html
- http://blog.kazuhooku.com/2013/11/blog-post.html
- http://d.hatena.ne.jp/kazuhooku/20131118/1384781154
- http://www.slideshare.net/ockeghem/how-to-guard-your-password
秘情報漏洩対策(filter_parameters)
せっかく DB には平文パスワードを保存しないようにしても、ログファイルなどに残ってしまっては、漏洩対策としては完全ではない。
また、ログはアクセス解析などにも利用されるため、 DB の権限とは別であり、より広いスタッフにアクセス権が付与される可能性がある。
パスワードに限らず、クリティカルな情報や、ログ解析に不要な情報は、フィルタリングによってログから外しておくのが望ましい。
Rails での対策
Rails では標準でログがかなり丁寧に出る。
I, [2015-07-07T22:05:44.806136 #7600] INFO -- : Started POST "/users" for 127.0.0.1 at 2015-07-07 22:05:44 +0900
I, [2015-07-07T22:05:44.807300 #7600] INFO -- : Processing by UsersController#create as HTML
I, [2015-07-07T22:05:44.807343 #7600] INFO -- : Parameters: {"utf8"=>"✓", "authenticity_token"=>"SHW5TJg69U0nJNsj==", "user"=>{"name"=>"Jxck", "pass"=>"passswoorrdd", "mail"=>"mail@example.com"}, "commit"=>"Create User"}
Rails はログファイルに、指定した秘情報をフィルタリングする機能を、かなり初期から標準で備えている。
特に Password に限定したものではなく、運用に応じて必要なものを指定することができる。
# filter_parameter_logging.rb
Rails.application.config.filter_parameters << :password
I, [2015-07-07T22:05:44.806136 #7600] INFO -- : Started POST "/users" for 127.0.0.1 at 2015-07-07 22:05:44 +0900
I, [2015-07-07T22:05:44.807300 #7600] INFO -- : Processing by UsersController#create as HTML
I, [2015-07-07T22:05:44.807343 #7600] INFO -- : Parameters: {"utf8"=>"✓", "authenticity_token"=>"SHW5TJg69U0nJNsj==", "user"=>{"name"=>"Jxck", "pass"=>"[FILTERED]", "mail"=>"mail@example.com"}, "commit"=>"Create User"}
Password が [FILTERED]
となっていることがわかる。
config.log_level = :info
ちなみに SQL のログには出るが、こちらは通常開発時のみの表示で、 Production 環境では表示しないことが多いため、対象外である。
Started POST "/users" for 127.0.0.1 at 2015-07-07 22:56:52 +0900
Processing by UsersController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ookUNL9xYZ9etXg==", "user"=>{"name"=>"Jxck", "pass"=>"[FILTERED]", "mail"=>"mail@eample.com"}, "commit"=>"Create User"}
(0.1ms) begin transaction
SQL (0.2ms) INSERT INTO "users" ("name", "pass", "mail", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "Jxck"], ["pass", "paaaasssswwwwooord"], ["mail", "mail@example.com"], ["created_at", "2015-07-07 22:56:52.698820"], ["updated_at", "2015-07-07 22:56:52.698820"]]
(113.9ms) commit transaction
Redirected to http://localhost:3000/users/3
Completed 302 Found in 120ms (ActiveRecord: 114.2ms)
対策方針
基本的には Rails と同様の対策になると思うが、そもそもフレームワークに Logger が密結合しているかで変わってくる。
Logger の利用が任意であり、開発者が出力する値を明示的に選択するのであれば、開発者の責任として何もしないということになるだろう。
セキュリティ関連 HTTP ヘッダ
セキュリティに関わる HTTP ヘッダもいくつかある。
代表的なものとしては以下のようなものが上げられる。
- X-Frame-Options
- X-Xss-Protection
- X-Content-Type-Options
- Set-Cookie: secure; HttpOnly
- Content-Security-Policy
- Content-Security-Policy-Report-Only
- Strict-Transport-Security
- Access-Control-Allow-Origin
- Public-Key-Pins
HTTP のヘッダは、ミドルウェアやフレームワークのレイヤで、デフォルトのものを指定し、追加として開発者が明示的に指定するという分離が通常であり、その意味では下層は「余計なヘッダを追加しすぎない」のが行儀の良い実装と言える。
しかし、こうしたセキュリティのヘッダについては、影響範囲を考えた上で有効にする場合がある。どのヘッダをどの値でデフォルトとするかはデファクトがあるわけではないため、フレームワークの方針や対象としている用件によって変わる。
Rails での対策
Rails の scaffold を生成し webrick で起動すると、以下のレスポンスヘッダが付与されている。
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Request-Id: c84957b6-409c-4d3d-8f30-e390ad41a23d
X-Runtime: 0.023517
Set-Cookie: xxxx; path=/; HttpOnly
各内容は後述するとして、セキュリティという観点で見ると X-Frame-Option と X-Xss-Protection, X-Content-Type-Options そして、 Set-Cookie の HttpOnly 属性あたりが該当するだろう。
Rails が汎用的なフレームワークであることを考えると、アプリ開発への影響が比較的少ないヘッダが選ばれていることがわかる。
個々のヘッダごとに見て行く。
X-Frame-Options
そのページが、悪意のあるページ内で iframe を用いて埋め込まれ、視覚的にわからないように表示されると、そこを訪れたユーザが意図しない操作をさせられる可能性がある。これをクリックジャッキングという。
X-Frame-Options ヘッダは、レスポンスの iframe 内表示を制限するヘッダであり、以下の値を取る。
- DENY: フレーム内に表示させない
- SAMEORIGIN: 同一オリジンのみ表示可能
- ALLOW-FROM uri: 指定オリジンのみ表示可能
Rails ではデフォルトで SAMEORIGIN が追加されている。
デフォルトを DENY にした場合は、自分で開発しているアプリに閉じているにも関わらず、 iframe 読み込みができないと、ヘッダに関する知識が無い場合に、ハマる可能性がある。
一方で、外からの iframe 読み込みは、開発者が意識していない場合は攻撃として行われている可能性が含まれるため、これをデフォルトで除外する判断をしている。
つまり、 Rails では、自身のオリジンについては、開発者の責任範囲にあるという前提を取り、別オリジンから iframe 読み込みを行う場合はレールから外れており、開発者は意識的にヘッダをいじる必要があるということになる。
- https://developer.mozilla.org/ja/docs/HTTP/X-Frame-Options
- https://www.ipa.go.jp/files/000026479.pdf
X-Xss-Protection
IE8 からの機能。 XSS の可能性のあるリクエストとレスポンスの組を見つけると、反射型であると疑い、ブラウザが警告を出す XSS Filter という機能が実装された。
このヘッダは、その有効無効を切り替える。
以下の値を取る。
- 0: disable
- 1: enable
- mode=block:
#
以外表示しない。
Rails はこれを block モードで有効にしている。
X-Xss-Protection: 1; mode=block
誤検知の問題が少なからずあるうえ、 IE 独自仕様かつ挙動が不透明なため、個人的には本当にデフォルトにすべきなのか微妙だと感じているが、正しく検知できるのであれば特に拒む理由はなくなる。
ちなみにその是非は特に議論は無く導入されているようだ。
- http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx
- http://blogs.technet.com/b/srd/archive/2008/08/19/ie-8-xss-filter-architecture-implementation.aspx
X-Content-Type-Options
IE がコンテンツの中身を見る(sniff)して、誤ったコンテントタイプで取得したコンテンツを、別のコンテントタイプとして読み込む場合に起こる XSS が報告されてきた。
no-sniff を指定することで、この sniffing を抑止し、正しいコンテントタイプが指定されていない場合はコンテンツを評価しないようになる。
以下の値をとる
- nosniff
すると、以下の MIME タイプと一致しないコンテンツは、勝手に解釈されなくなる。
- CSS: "text/css"
- JS: "application/javascript", "text/javascript", etc
Rails のレールに限らず、正しい Content-Type を指定すべきであるため、 IE の content type sniffing に頼った実装などすべきではない。 Rails はそれらを最低限サポートしているため、このヘッダがついていることで問題になることは、新規に開発しているものについては無いはずである。
- https://msdn.microsoft.com/ja-jp/library/gg622941(v=vs.85).aspx
- https://github.com/blog/1482-heads-up-nosniff-header-support-coming-to-chrome-and-firefox
Set-Cookie
Set-Cookie にはセキュリティに関する以下の属性をとれる。
- secure: HTTPS 通信でしかヘッダに付与されなくなる
- HttpOnly: HTTP ヘッダにのみ使用されスクリプト(JS etc)から触れなくする。
フレームワークのセッション実装には大きく二つの実装がある。
- 全てのデータをシリアライズして Cookie に入れる
- Cookie には ID のみを入れ、 ID と紐づけたオブジェクトをサーバ側に保持する
Rails はこの実装方式を設定で切り替えることができるようになっているが、基本的にはその値はサーバ内でしか触らないことになっている。
もしクライアントでそれを触りたい場合は、 View をレンダリングする際に適切な DOM のプロパティに値を保存するなどの処理を通じて送ることが多い。
したがって、 Rails では Cookie を JavaScript から触る必要はあまり無いため、 HttpOnly をデフォルトで送っている。
一方、現在 Rails は HTTPS を前提とはしていないため、 secure 属性はついていない。
対策方針
ヘッダの中には、 HTTPS の場合に効果を発揮するものもある。
多くのフレームワークは、作られるアプリが HTTP か HTTPS かを規定しないため、 HTTPS に関するヘッダをデフォルトとすることはあまりない。
また、実際運用上 HTTPS でデプロイされるとしても、アプリは HTTP で作り、その前段のプロキシなどで HTTPS 化(TLS 終端)する構成が多いため、フレームワーク自体がこれを付け加えるのではなく、フレームワークのレスポンスにプロキシが追加する構成もある。
また現在は、開発時はローカルで HTTP で開発することが主流であるため、 HTTPS 前提のアプリを開発する場合は、開発環境の考慮も必要となる。
一方で、 HTTPS の重要性は増しているため、将来的にデータセンタ内の暗号化もみすえて、アプリも含めてフレームワーク自体が HTTPS を前提とする自体がくれば、フレームワークがデフォルトでこれらのヘッダを付ける可能性も出てくるだろう。
それを踏まえた上で、以降は、ここまでに出ていないものを紹介する。
Content-Security-Policy(-Report-Only)
外部リソースの読み込みについての制限を細かく決めることができる。
しかし、フレームワークがデフォルト値を制限するのは難しいと思われる。
Access-Control-Allow-Origin
デフォルトで Same Origin Policy の制約を受ける XHR が、クロスオリジンアクセスを許容するためのヘッダ。
性質上、クロスオリジンなアクセスを許可する場合に、ホワイトリストで指定するのが通常であるため、デフォルトという意味では付けないでよい。
Strict-Transport-Security
HSTS を有効にするためのヘッダ。
次から HTTP のリクエストも HTTPS にリクエストに読み替えられる。
また、ブラウザがあらかじめ読み込んでいる Preload-HSTS のリストに載せられれば、最初のリクエストも HTTPS にできるため、より完全な HTTPS 化が可能となる。
Public-Key-Pins
CA への攻撃が成立し、不正な証明書が発行されるのを防ぐために、正規の証明書を HTTP ヘッダ経由でクライアントに送り、検証可能にするもの。
ブラウザの実装に組み込む preload もある。これにより不正な証明書発行が発見されるケースもある。
逆に証明書の更新時にヘッダの更新がずれるとサイトが見られなくなるため、運用には注意が必要。
これも HTTPS を前提とする場合以外は必要ない。
link
これからの Web 開発のセキュリティ
ここまで Rails の例をもとに、 Web 開発時に行われているセキュリティ対策について見てきた。
特にセキュリティ対策の定番である、 XSS, CSRF, SQLInjection 対策などの一般的なものについては、 Rails を普通に(レールに乗って)使っていれば対応できることがわかるだろう。
個人的には、こうしたフレームワークを用いて開発すれば、既知の脆弱性については十分対策が可能な程度に、サーバサイドのセキュリティは成熟してきていると考えている。
今後、それをより強化していくために必要な視点は主に二つ考えられる。
- HTTPS 化
- CSP
- フロントエンドのセキュリティ
- 新しいパラダイムと API
HTTPS 化
スノーデン事件で、NSA による広域盗聴の可能性が発覚したこともあり、 HTTPS の重要性が叫ばれるようになった。
これまではログイン画面だけを HTTPS 化するというパターンも多かったが、 Google や Twitter などの大手サイトは既にフル HTTPS 化を完了している。
また、この流れは新しく出てくるプロトコルにも影響を与えている。例えば HTTP2 は HTTP も仕様に含まれているが、多くのブラウザは HTTPS での実装しか対応しないという流れがある。また、 Service Worker などの新しい API は HTTPS でしか動かなかったり、 getUserMedia
など、ユーザに許可を求める API は HTTPS でないと毎回許可を求める挙動となっている。
今後もこの流れが加速する場合は、フレームワーク自体も HTTPS を前提としたものになっていく可能性も考えられるだろう。
CSP
XSS の対策として、 CSP の有効化は非常に有効である。
一方で、広告などの外部コンテンツを埋め込むサイトでは、一概に CSP の有効化をできるとは限らない。
しかし、埋め込まれるコンテンツを自分で管理できるサイトについては、より積極的に CSP を有効にするのが望ましいだろう。
その場合、コンテンツの管理や inline hash の埋め込みなど、フレームワークがサポートできる範囲は広いため、 CSP を前提としたレールを敷くことの需要は少なくないと考えられる。
フロントエンドのセキュリティ
近年の Web アプリケーションでは、サーバサイドと同等かそれ以上にフロントエンドでのロジックが多いものも少なくない。
サーバサイドだけで対策をしていたとしても、フロントエンドでの対策が甘ければ DOM Based XSS などの攻撃を防ぐことはできないため、
必然的に、フロントエンドでのセキュリティ対策の重要性は、今後より高まっていくことが予想される。
現在、サーバサイドとフロントエンドのセキュリティ対策は、それぞれ別に行われている場合が多いため、今後は両方を合わせた全体に対してセキュリティ対策のレールを敷くことが求められていくのではないかと筆者は考えている。
しかし、まだフロントエンドとサーバサイドの開発両方を十分に包括したフレームワークには決定版が無いため、包括的なセキュリティ対策を可能とするためにはそうしたフレームワークが成熟が必要である。この分野には、まだまだ研究の余地が多分にある。
新しいパラダイムと API
Service Worker によるリクエストの Proxy や、 WebRTC による Local IP の取得など、これまで Web/ブラウザではできなかった事が、新しい API の登場によって可能になって来ている。
こうした API は、「どのように使えば安全か」以前に、それらの利用によるリスクなども未知の部分が少なく無い。
こうした機能を、どのようにフレームワークレベルでレールを敷いて行くのかは、考えていく必要がある。
最後に
Web のセキュリティ研究や実践の結果、ベストプラクティスと言われる開発ノウハウや、対策のためのブラウザ API などは進歩している。しかし、一方でセキュリティ自体を専門としていない開発者が、それら全てを把握して開発に取り込むのは、非常に難しくなってきている。
Web セキュリティの専門家が導き出したノウハウを、 Web 開発者が効率よく取り込む手法の一つとして FW があると考える。
FW が適切な形でその対策を取り込んでいるか、そのための API とガイドラインをもっていれば、開発者は少ない知識でも安全なアプリを開発することが可能になるだろう。
セキュリティを専門として極めて行く場合は、その成果を開発者に浸透させる手段の一つとして Web アプリケーションフレームワークという手段があることを利用し、フレームワークへの貢献も是非視野に入れて考えてみて欲しい。
Jxck