freeeのkakkunpakkun(略してkp)といいます。マイクロサービスおじさんです。
freeeではモバイルバーサーカーとかフロントエンド革命家など、独自の名前が多いですが、私は「分割おじさん」「認証おじさん」「rspecおじさん」などと呼ばれたりしてきました。
バーサーカーや革命家と比べてかっこ良くないのがなんだか気になる今日このごろです。
今日はおじさんらしく細かくて面倒な話をして、その対策としてfreeeで使ってるgemを公開したのでその説明をしようと思います。
パスワードはDBでハッシュ化していても簡単に見えてしまう
「パスワードが見えてしまう」というのはすごい不安になる言葉ですが、あまり考えずにサービスを開発していると簡単に起こる事象です。
ではどこでそういうことが起こりやすいのでしょうか。
DBにハッシュ化したパスワードを保存するのはかなり一般的になり、各種ライブラリも対応しているためDBにはパスワードがそのまま保存されたりすることはかなり減ったかと思います。
しかしこれまでの経験上logにはパスワードがそのまま記録されているという事故は起こりやすいです。しかもそれに気づいてもいないこともままあります。
どういう時に?Railsのデフォルトの挙動を確認
freeeではRailsを使っているのでRailsで確認してみます。
Railsで確認しますが、基本は他のWAFでも似たようなことが起こるのではと思います。
Rails4.2.5で標準のloggerで確認します。
rails newしたばかりのアプリケーションにコントローラーを作って叩いてみるとpasswordは[FILTERED]
になっていて、パスワードが見えない状態になっています。
うれしい挙動ですね。
Processing by UsersController#create as HTML
Parameters: {"utf8"=>"✓", "user"=>{"name"=>"kp", "password"=>"[FILTERED]"}, "commit"=>"Create User"}
ここではparamsのauthenticity_token
は例を示す上で邪魔なので削除しています。
これは、initializersにあるfilter_parameter_logging.rbというファイル内でpasswordが含まれるキーの値にはfilterが掛けられているためです。
Rails.application.config.filter_parameters += [:password]
symbolが渡されると/password/i
と同じ働きになるのでpassword
が含まれていれば全部[FILTERED]
になります。
ネストされていても[FILTERED]
になります。
この辺の挙動はActionDispatch::Http::FilterParametersのドキュメントに書かれていて、filterの機能はActionDispatch::Http::ParameterFilterで提供されています。
なので、よくあるパスワードの二度入力を求める場合にも'password'
という文字列が含まれるparameter名にしておけば以下のように全て[FILTERED]
になります。
Started POST "/users" for ::1 at 2015-12-07 14:00:41 +0900
Processing by UsersController#create as HTML
Parameters: {"utf8"=>"✓", "user"=>{"name"=>"kp", "password"=>"[FILTERED]", "user_password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create User"}
これもありがたいですね。
では試しに設定をコメントアウトして再起動してみましょう。
# Rails.application.config.filter_parameters += [:password]
そうすると以下のように入力されたpasswordが表示されてしまいます。
Started POST "/users" for ::1 at 2015-12-07 14:05:27 +0900
Processing by UsersController#create as HTML
Parameters: {"utf8"=>"✓", "user"=>{"name"=>"kp", "password"=>"pass"}, "commit"=>"Create User"}
これはマズいですね。いくらDBにpasswordをハッシュ化して保存しててもここに出力されていては意味がないです。
一度本番環境でproduction.log
を確認することをおすすめします。たぶんギョッとする方もいると思います。
パスワード以外の出したくない情報
ここまでは分かりやすいように「パスワード」と言っていますが、クレジットカードのセキュリティコード(cvc,cvvなど)、各種_secretなども同様で、取り扱いに気をつけないとこれらのヤバい情報がどんどんlogファイルなどに出力されてしまいます。
やっぱり気になるのはクレジットカード情報ですよね。
同じデフォルトの設定のままcard_number
, cvc
を入れてもらう決済画面を作ったらどうなるでしょう?
Processing by CreditCardsController#create as HTML
Parameters: {"utf8"=>"✓", "credit_card"=>{"card_number"=>"1234123412341234", "cvc"=>"123"}, "commit"=>"Create Credit card"}
やっぱり出てしまいましたね。
こういう感じで本来logファイルに出すべきではない情報は結構うっかり出てしまうものです。
しかも、アプリケーションが育っていくに従って出したくない情報が増える事もあります。
なぜヤバい情報を出したくないか
当たり前のことですが改めて確認しましょう。
まずはログファイルなど何らかの形で保存されるというのがまずいところでしょう。
もちろんそのファイルをWebアプリケーションで表示することはないでしょうが、それゆえに甘く見られてしまうこともある気がします。
保存されているまずさをRailsGuidesでもこのように書いています。
When designing a web application security concept, you should also think about what will happen if an attacker got (full) access to the web server. Encrypting secrets and passwords in the database will be quite useless, if the log files list them in clear text.
Webアプリケーションのセキュリティを考える以上、Webサーバにフルにアクセスされてしまうことを考慮すべきで、その時にいくらDBに暗号化(encrypt)して保存してあってもlogに平文のまま出力されてしまっていては意味が無いというわけです。
なので、内部しかアクセス出来ないlogファイルでもきちんとfilterをしましょう。
またクレジットカードのcvc, cvvなどのセキュリティコードはそもそも保存が禁止されてます。
logファイルに書き出されるのも保存になるので気をつけましょう。
どういう時にヤバい情報が出てしまうのか
じゃあ、どういう時にヤバい情報が表示されてしまうのでしょうか。
うっかりparamsを叩いてloggerで出力
まずは普通にparams[:password]
を叩いたりするとpasswordが出てしまいますね。
logger.info(params[:password])
もちろんなかなかこうやって叩くことはないと思います。
やっていたら止めましょう!
でも
logger.info(params)
みたいにしてても以下のように出力されます。
{"utf8"=>"✓", "user"=>{"name"=>"kp", "password"=>"pass"}, "commit"=>"Create User", "controller"=>"users", "action"=>"create"}
これはparamsの中身が[FILTERED]
になっているのではなく出力時に[FILTERED]
を表示しているためです。
paramsの中身はアプリケーションが使うので当たり前といえば当たり前ですね。
というわけで、filter_parametersは設定していてもparamsの取り扱いは気をつけないと次々とlogなどにヤバい情報が出力されていきます。
paramsを独自loggerに渡す
独自のログシステムに出力する場合とかはどうでしょう。
Webサービスの場合は複数のサーバに分かれることは当然起こることで、それらのログファイルをまとめて見れるように工夫することは多いです。問題検知用だったりデータ分析用だったり様々なところで考えられるかと思います。
そういう独自のロガーにデータを渡すときは問題が起こりやすいです。
例えば分析用のdokuji_logger
を作成して、それに渡す時
dokuji_logger.log(params)
こうすると便利だけど、paramsが渡り、その中では普通にpasswordなどのヤバい情報が渡されてしまいます。
バグ通知サービスなどの外部サービス
sentry, bugsnag, airbrakeなどのバグ通知サービスはすごく便利ですが、これらもうっかりすると出したくない情報が出てしまうことがあります。
提供されているライブラリによってはRails.configuration.filter_parameters
の設定を利用して[FILTERED]
にしてくれることもありますが、対応はまちまちだなという印象です。
bugsnagの場合最新のGemは、
By default, params_filters is set to [/authorization/i, /cookie/i, /password/i, /secret/i], and for rails apps, imports all values from Rails.configuration.filter_parameters.
と、結構手厚いです。
しかし、他にもSaaSなどを使っているとどんどん同じようなことが起こります。
というわけで、ヤバい情報はそもそも渡さないようにするのが良さそうです。
問題
しかしここで問題が起こります。
一つのRailsアプリだけでサービスを運用しているなら一回設定すれば良いかもしれませんが、たとえばfreeeの場合は複数のRailsアプリケーションによるサービス提供をしているし、新たなRailsアプリケーションが作られるタイミングで同じ設定をして回らないといけません。
しかも設定をしてあってもparamsの取り扱いを間違えると結局うっかり出力されてしまします。
さすがに面倒だし、だからといって面倒がってちゃんとしないとかなりまずいです。
[FILTERED]
なparamsを提供するblinkers
というわけで、そういう基本filterをかけそうなキーをデフォルトで設定し、filterされたsecure_params
という機能を備えた小さなgemを作成しました。
blinkersというgemで元々社内で使っていたのですが、せっかくなので公開することにしました。
secure_params
はparamsにActionDispatch::Http::ParameterFilter
でfilterした結果を返すメソッドです。
{"utf8"=>"✓", "credit_card"=>{"card_number"=>"[FILTERED]", "cvc"=>"[FILTERED]"}, "commit"=>"Create Credit card", "controller"=>"credit_cards", "action"=>"create"}
いつでもどこでも[FILTERED]
になります。
freeeでは外部サービスやloggerに渡す必要があるときはsecure_params
を使うようにしています。
これならRailsで設定さえされていればうっかりヤバい情報が出ないで済みます。
特に理由がない限りは全部secure_params
にしてしまってもいいのではないかとも思ったりしています。
運用的な対応
あとは運用的な対応もやっておいた方がいいと思っています。
password
, secret
, cvc
, cvv
, security_code
などは統一した名前を使うようにしましょう。
pass
など同じものを表す違う名前を付けないようにするとfilter_parametersの設定も多くならず、楽です。
特にセキュリティコードはcvv
, cvv2
, cvc
など表現の種類が多いので内部ではなにかに統一するようにするのも手です。
というわけで、
freeeではヤバいことが起こらないようにしつつサービスを成長させるエンジニアを募集しています。よろしくお願いします。
明日は、freeeのQiitaストック数ナンバーワン、ソーシャル力ナンバーワンのナンバーワン担当エンジニアの @futoase さんです。
2015-12-12 補足追記
当初パスワードの暗号化という言葉を使っていましたが、ハッシュ化と言った方が正しいのでハッシュ化に変えました。