概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Session-only cookie corruption in Ruby web apps | perlkour
- 原文公開日: 2017/11/21
- 著者: perlkour
Ruby製Webアプリのcookie-onlyセッション破損でセキュリティリスクの可能性(翻訳)
RackやRailsにはクッキーモンスターがいます。
ブラウザは、ドメインやレスポンス内でcookieの数やサイズに制限を設けています。この制限を超えると、まずいことが起こる可能性があります。明らかなケースについてはRackやRailsが防止を試みますが、本記事では現在の実装で生じる問題について説明します。また、RubyのWebアプリで発生する問題の潜在的なインパクトや、問題の緩和方法についても検討します。
この情報は、(セッションIDに限らない)セッションハッシュ全体をエンコードした内容を含んでいるセッションcookieを転送するWebアプリと強く関連します。つまりcookie-onlyセッションのことです。MarshalやJSONを用いるRack::Session::Cookieや、RailsのデフォルトのActionDispatch::Session::CookieStoreや、(専用のレスポンスヘッダの代わりに)cookieを転送に用いるJWT(JSON Web Tokens)の実装などがそうです。cookie-onlyセッションが、cookieを切り詰める(truncate)古いブラウザの挙動に遭遇すると、セキュリティリスクが最大になります(このときFlashメッセージなどの任意の巨大データもセッションに含まれている可能性があります)。
手短に言うと、Rubyアプリでcookie-onlyセッションを使う場合は、Rack::Protection::MaximumCookieをミドルウェアスタックに追加することを検討しましょう。
ブラウザのcookie制限
制限の正確な内容はブラウザごとに異なっています。ブラウザにはcookieの個数やサイズの制限がありますが、本記事では後者の「サイズ」に着目します。Chrome、Edge、Firefox、Safariといった定番ブラウザでは、「cookie単位」の比較的寛容なサイズ制限が設けられています(かつ、制限を超えた場合の挙動はおかしくありません)。しかし古いブラウザではこれと対照的に、(「cookie単位」のオーバーヘッドの有無にかかわらず)サイズ制限が「ドメイン単位」になっており、サイズを文字数またはバイト数でカウントしていることがあります(制限を超えるとcookieは切り詰められます)。
定番ブラウザの場合
最新(記事執筆当時)のデスクトップ向け定番ブラウザをテストした結果、以下の制限が明らかになりました。
- Chrome 64: cookie単位: 4,095バイト、ドメイン単位: 180
バイト個 - Edge 38: cookie単位: 5,117バイト、ドメイン単位: 10,234バイトおよびcookie 50個(Ianに感謝!)
- Firefox 56: cookie単位: 4,096バイト、ドメイン単位: cookie 150個
- Safari 11: cookie単位: 4,096バイト
ブラウザが制限値を超えると、単にcookieの破棄を開始します。これによって、たとえばログインしていたはずのユーザーが知らないうちにログイン画面に戻されたりする可能性が生じます。何らかの防御対策が行われていなければこの現象の発生に気づけないかもしれません(ユーザーから苦情が寄せられれば別ですが)。
ここで強調しておきたいのは、サイズ制限の対象がcookieのキーや値のみならず、あらゆるディレクティブにも適用されるという点です。たとえば、key=value
文字列のcookieがUTF-8で4,096バイトだとすると、Set-Cookieに; path=/
を追加するだけで制限値に達し、上述のあらゆるブラウザで警告なしにcookieが破棄されることがあります。実際、; path=/shop;domain=example.org; expires=Sat, 04 Nov 2017 00:02:20 -0000;Secure; HttpOnly; SameSite=Strict
のようなディレクティブはかなりのスペースを消費することがあります。
古いブラウザの場合
注意: 簡単に言うと、「古いブラウザ」にはIE11のように現代的だが定番ではないものや、iOS向けSafariのようなモバイルブラウザも含まれる可能性があります。
古いブラウザにおける制限はバラバラです。この資料はかなり古ぼけていますが、ブラウザの種類やバージョンによってどれだけ制限値がばらついているかをざっくり知るにはよいでしょう。注目したいのは次の2点です。
- 典型的なサイズ制限は「ドメイン単位」であり、「cookie単位」ではない
- 一部のブラウザでは「cookie単位」のオーバーヘッドが生じる
おそらく例で説明するのが一番わかりやすいでしょう。あるブラウザでは、cookieのサイズがドメイン単位で4,096バイトに制限され、cookie1つあたり3バイトのオーバーヘッドが生じているとしましょう。(example.orgなどの)どんなドメインを指定しても、4,093バイトのcookie 1個か、4,000バイトのcookie 1個と90バイトのcookie 1個か、1,000バイトのcookie 4個と81バイトのcookie 1個か、などのようになります。cookieサイズの合計とcookieごとのオーバーヘッドは、この制限値を超えてはなりません。
定番ブラウザとのもうひとつ重要な違いは、古いブラウザは制限値を超えたときにcookieを切り詰めてしまう可能性があるという点です。これは次の2つの理由によって深刻な問題です。
- データやダイジェストが破損し、セッションが不正になる可能性が生じる
Secure
、HttpOnly
、SameSite
などのディレクティブがcookieから脱落する可能性がある
Secure
ディレクティブが脱落すると、ブラウザが誤ってこのcookieをセキュリティで保護されていない接続で送信され、悪意のある第三者がcookieを盗んだり悪用したりする可能性が生じます。HttpOnly
ディレクティブが脱落すると、cookieはXSS(クロスサイトスクリプティング)攻撃(JavaScriptからアクセス可能になるなど)に対して脆弱になります。SameSite
ディレクティブが脱落すると、cookieはCSRF(クロスサイトリクエストフォージェリ)攻撃に対して脆弱になります1。
RackとRailsの場合
明らかにcookieのサイズ制限は今に始まった話ではありませんし、RackやRailsのメンテナーはそれなりにこの点を認識しています。Rack::Session::Cookie
やActionDispatch::Session::CookieStore
では、key=value
文字列のサイズが4,096バイト(または文字)を超えていないかどうかをチェックすることでこの問題の防止を試みます。しかしこの戦略は、以下の理由から有効ではありません(ここまでお読みいただいた方であればきっとおわかりいただけるはずです)。
- 戦略にディレクティブが含まれていない
- cookie単位のオーバーヘッドが考慮されていない
- ドメイン単位のcookieサイズが加えられていない
さらに、key=value
文字列は初期段階のサイズチェックをパスすることがあり、このときにRack::Utils
のescape
メソッドを巧妙に用いたある種のエンコーディングは、文字列を肥大化させて制限値を超えさせてしまう可能性があります。Rack::Session::SmartCookieのREADMEでサンプルをご覧いただけます。
JWTについて
cookie転送を用いるJWTの実装は、独自の制限チェックを実行しなければなりません。
解決方法
RackやRailsでこの潜在的な問題が修正されるまで新規や既存のWebアプリを保護する最善の方法は、正しいcookie制限チェックを実装した小さなミドルウェアを追加することです。このミドルウェアは、制限値を超えたときにエラーをraiseし、アプリレベルで修正対応が取れるようにします。
私の公開したRack::Protection::MaximumCookie gemは、この必要を短期または中期間満たすためのものであり、JWTやその他のユースケースについても同様に修正します。記事執筆時点では作業中ですが、デフォルト設定はほとんどのWebアプリで適切なものになっているはずです。設定で対象を定番ブラウザに限定したり、慎重さのレベル(levels of conservativeness)を上げたりすることもできます。注意書きをよくお読みください。皆様からのフィードバック、ご意見ご感想、プルリクを歓迎いたします。
- この問題は10/28にRackコアチームに、11/3にRailsセキュリティチームにそれぞれ最初に報告されましたが、著しいセキュリティリスクとは見なされていません(そう見えます)。私のgemと本記事は、これらのチームが応答する時間を十分確保できるよう11/20まで保持しました。
関連記事
-
まあ確かに、複数のブラウザの共通部分が
SameSite
を実装し、かつサイズ制限を超えたcookieを切り詰める可能性は、ゼロではないにしてもおそらくきわめて小さいと思われます。 ↩