XSSリスクを考慮した上で、トークンをin-memory、localStorage、Cookieのどれに保存する?
以前から、SPAと呼ばれる形態を取るWebアプリにおいてトークンをどこに保存するべきか悶々としていたので、ここらで簡単にまとめておきます。
なお、自分はサイバーセキュリティの評価を主要事業とする企業に勤めているわけではないので、内容が間違っているかもしれません。 特に、世界的な攻撃の動向を踏まえた客観的なリスク評価については専任で従事されている方に敵わないのでお察しください。
トークンって何?どこに保存するのか選ぶのってそんなに大事なの?という疑問をお持ちの方は Auth0の『Token Storage』を読むか、 「token localstorage cookie」というキーワードで Google 検索していただくと多くのサイトで考察されていますのでそちらをお読みください。
「トークン」の定義
この記事ではAPIを呼び出すために必要な認証用の文字列として定義します。
保存方式比較
比較表
いったん私が考えている比較表をお見せします。 各項目は相対的な二段階評価で記入しています。 同じ「高い」でも掘り下げていけば差は出てくると思いますが、話が難しくなるので今回はシンプルにまとめています。
in-memory グローバル | in-memory ワーカー | localStorage | Cookie | |
---|---|---|---|---|
保持できる期間 | 短い | 短い | 長い | 長い |
クロスオリジンの制約 | 弱い | 弱い | 弱い | 強い |
サードパーティクッキーの制約 | なし | なし | なし | あり |
容量 | 大きい | 大きい | 大きい | 小さい |
特定サイトを狙ったXSSの耐性 | 低い | 低い | 低い | 低い |
広範囲を狙ったXSSの耐性 | 低い | 高い | 低い | 高い |
in-memory の分割
in-memory の保存方式をグローバル方式とワーカー方式に分けています。
グローバルスコープ方式は JavaScript のグローバルスコープに格納する方式です。
ワーカー方式は JavaScript のワーカーに格納する方式です。一応クロージャーで隠ぺいしたものもこちらにカテゴライズすることにします。 この方式は Auth0 の『Browser in-memory scenarios』にて説明されています。
攻撃の種類
今回注目するのはXSSですが、同じXSSでも攻撃のレベルを分類することができるだろうというのが私の考えです。 今回は特定サイトを狙うか、広範囲を狙うかで分類しています。
特定サイトを狙う | 広範囲を狙う | |
---|---|---|
同じコードの流用 | 難しい | 易しい |
開発コスト | 高い | 低い |
対象 | 公開されている情報価値の高いサイト | 内部サイトも含む数多のサイト |
もっと細かく分類できると思いますが、重箱の隅をつつき出すと話が終わらなくなるので、この二つに分類しています。
特定サイトを狙うシナリオの例
- 特定サイトを調査して、サイト固有の XSS 脆弱性を見つけ出し、正規の利用者に攻撃コードを含むリンクを踏ませる
- 正規品を装った npm パッケージに特定サイトに向けた攻撃コードを仕込んでおき、特定サイトのドメインが読み込まれたときに起動するようにしておく
広範囲を狙うシナリオの例
- 正規品を装った npm パッケージにグローバルスコープ変数、localStorage、非HttpOnlyのCookie を根こそぎ奪うコードを仕込んでおく
- プラットフォームの脆弱性を見つけ出し、そのプラットフォームを使用している複数のサイトの利用者に攻撃コードを実行させる
localStorage や in-memory グローバル の方式は広範囲を狙う攻撃に弱い
localStorage や in-memory グローバル の方式は、簡単なスクリプトで情報を根こそぎ奪い取ることができます。
また localStorage やグローバルスコープの変数の参照方法はどんなサイトでも共通であり、同じコードで多くのサイトを攻撃することができます。
例えば localStorage の場合は localStorage.key(number)
という関数で、どんなキーが入っているのかスキャンすることができます。
上記の理由から、広範囲を狙う攻撃に弱いと思われます。
情報を根こそぎ奪うというのは攻撃者にとっては魅力的で、トークンを盗むついでに価値の高い情報を盗み出せるかもしれません。 むしろトークンを盗むこと自体が副目的になる場合すらあり得ます。
in-memory ワーカー や Cookie の方式は広範囲を狙う攻撃に強い
対して、in-memory ワーカー や Cookie の方式は、変数がどのように格納されているのか、どういった通信を行えば情報を引き出せるのかといった調査が必要になるため、 広範囲を狙う攻撃に強いと思われます。
特に Cookie は HttpOnly 属性さえつけておけば JavaScript から直接読み出されることはないため、情報を引き出すには何らかの通信を発生させる必要があります。 攻撃の際はこの通信の仕様を調査する必要があるため攻撃のコストは相対的に高くなると思われます。
特定のサイトを狙った攻撃はどの方式でもあまり変わらない
特定のサイトを狙う場合は、攻撃者がそれなりのコストをかけることを承知の上となるため、どの保存方式をとったとしても耐性にあまり差は無いと思われます。
例えば JavaScript ワーカーへ強固にトークン保存したとしても、スクリプトからボタンを押させ、DOMツリーに読み込まれた機密情報を盗み出すことができます。 Cookie へ HttpOnly 属性付きでトークンを保存したとしても、スクリプトから Web リクエストを発生させ、そのレスポンスから機密情報を盗み出すことができます。
WebサイトにXSS脆弱性があるとして、トークンの保存方式によって影響を「受ける」「受けない」の二元論で語ると、どんな保存方式でも「受ける」ことになります。
各方式のリスクに「大差」はあるか?
これは正直、ノーコメントです。 私は、どこからが大差なのか閾値を客観的に設定できるほどデータを持っていないためです。
少なくとも攻撃のレベルを考慮した上での差はあると思います。 これは、個人的な感覚ですが、無視できないレベルの差だと思います。
ただし、この差を大差だという人もいると思いますし、僅差だという人もいると思います。
どの方式を選ぶべきか?
例えば以下のような基準が設定できるかと思います。
- in-memory グローバル
- 容易に実現できるほぼ上位互換の代替方式(in-memory ワーカーあるいはlocalStorage)があるため基本的に採用しない
- localStorage
- Cookie
- 上記以外の場合
まとめ
この話題が登場して、頻繁に取り上げられるようになってからそろそろ5年くらい経ちますかね。 それでもいまだ解決を見ないのは悩ましい問題です。
個人的には Auth0 の『Token Storage』がバランスのいい説明だと思っています。 ただリスクの面にまで突っ込んだ解説をされているわけではないので、その辺りは世の中のエンジニアが考察してアウトプットしていくしかないのかなと思う次第です。