🐶

アクセストークンの保管場所のベストプラクティスを求めて〜XSSとCSRFの対策〜

に公開2

はじめに

アクセストークンの保管場所に関しては、以下のように「localStorageには保存すべきではない」や「HttpOnly Cookieに保存すべきだ」など、様々な議論があります。

そこで、個人開発でJWT認証の実装を行う中で、XSSやCSRFといった攻撃への対策を考慮しつつ、アクセストークンの保管場所についてベストプラクティスを追い求めたので、本記事に残したいと思います。

 

↓議論
Please Stop Using Local Storage - Randall Degges
訳)ローカルストレージに秘密情報を保管するのは、セキュリティが最弱の地下金庫に秘密情報を保管することと同じです。「JWTをローカルストレージに保存する」という誤った情報を教えている人が五万といますが、それは誤りです。そのような人からは、離れてください!

Think about it like this: when you store sensitive information in local storage, you’re essentially using the most dangerous thing in the world to store your most sensitive information in the worst vault ever created: not the best idea.
There are thousands of tutorials, YouTube videos, and even programming classes at universities and coding bootcamps incorrectly teaching new developers to store JWTs in local storage as an authentication mechanism. THIS INFORMATION IS WRONG. If you see someone telling you to do this, run away!

HTML5 Security Cheat Sheet - OWASP
訳)クロスサイトスクリプティングはlocalStorage内の全ての情報を盗むことができるので、localStorageに秘密情報を保存しないでください。
localStorageにセッション識別子を保存すると、JavaScriptからアクセスできてしまいます。HttpOnly Cookieに保存することでこのリスクを軽減できます。

A single Cross Site Scripting can be used to steal all the data in these objects, so again it's recommended not to store sensitive information in local storage.
Do not store session identifiers in local storage as the data is always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag.

徳丸さんのスライド

CookieよりもlocalStorageの方がXSSに対して危険という記事を多く見ますが、一概には言えません...
CookieとlocalStorageはどちらが安全とは言えず一長一短
適材適所で使えばいい

そもそもどんな脆弱性が存在するのか?

そもそもアクセストークンの保管場所を検討する際に、どのような脆弱性に気をつけるべきでしょうか。その代表例が、XSSCSRFです。

XSS(Cross-Site Scripting)

XSS(クロスサイトスクリプティング)は、XSS脆弱性のあるWebアプリに対して、攻撃者が悪意のあるスクリプトを埋め込み、それをユーザーのブラウザ上で実行させる攻撃手法です。
 
以下がXSS攻撃の流れです。

CSRF(Cross-Site Request Forgery)

CSRF(クロスサイトリクエストフォージェリ)は、Webアプリケーションにログイン済みのユーザーに対して、不正なリンクを踏ませるなどして、ユーザーの持つ認証情報(Cookie)を勝手に使用して、ユーザーの意図しないリクエストを送信する攻撃手法です。

以下がCSRF攻撃の流れです。

LocalStorage

JWTをlocalStorageで保管することのメリット・デメリットは以下です。

メリット

  • 実装が容易
  • タブを閉じても永続的に保管される
  • 異なるタブ間でも共有されるため、複数のタブでログイン状態を維持できる
  • AuthorizationヘッダでJWTを送信するため、CSRF脆弱性なし

デメリット

  • XSS攻撃で不正なJavaScriptからlocalStorage内のデータを盗むことができる
    → ただし、localStorageに保存されたすべてのデータにアクセスできるわけではありません。JavaScriptは同一オリジンのlocalStorageにのみアクセス可能であり、異なるオリジンのlocalStorageにはアクセスできません。これは、SOP(同一オリジンポリシー)によって制限されているためです。

また、Please Stop Using Local Storage - Randall Deggesで指摘されているように、
自身で開発したアプリケーションにXSS脆弱性が存在しなければ、localStorageにJWTを保管しても問題ないように思えます。
しかし、XSS脆弱性を完全に排除することは極めて難しいとされています。なぜなら、Reactやbootstrapなどの外部リソースにXSS脆弱性が存在する場合、それがアプリケーション全体の脆弱性につながる可能性があるからです。

Cookie

JWTをCookieに保管することのメリット・デメリットは以下です。

メリット

  • HttpOnly属性をtrueに設定することで、JavaScriptからCookieにアクセスできなくなるため、XSS攻撃でJWTを盗むことはできない
    → HttpOnly属性のないCookieの場合、JavaScriptからCookieにアクセスできるため、localStorageと同様にXSS攻撃で盗まれるリスクがあります。

デメリット

  • CSRF脆弱性が存在する(sameSite属性をstrictに設定することで対応可能)

ただ、フロントエンドwww.example.comとバックエンドapi.example.comが異なるオリジンの場合、Cookieの使用難易度が上がります。それは、CSRF対策でsameSite:strictを設定すると、www.example.comからapi.example.comへの正当なクロスオリジンリクエストであってもCookieが送信されず、認証情報をバックエンドに送信できなくなるからです。
この場合、sameSite:noneを設定した上でCSRFトークンを導入することで、CSRF対策を行いつつ、www.example.comからapi.example.comへのCookieの送信が可能になります。

in-memory

JWTをブラウザのメモリに保管することのメリット・デメリットは以下です。

メリット

  • メモリ上のJWTを直接盗むことは難しいため、XSS脆弱性が軽減(不正なJSがトークン取得フローを実行するなどして、JWTを盗むことは可能)
  • AuthorizationヘッダでJWTを送信するため、CSRF脆弱性なし

Auth0の公式ドキュメントでは以下のように、トークンの保管場所として、localStorageではなく、メモリを推奨しており、Auth0はデフォルトでin-memory方式を採用しています。

Auth0は、最も安全なオプションとして、トークンをブラウザのメモリに保存することを推奨しています。

ブラウザーのローカルストレージにトークンを保管すると、ページが更新されても、ブラウザータブが切り替わっても、持続性を確保できるというメリットがあります。しかし、攻撃者がクロスサイトスクリプティング(XSS)によってSPAでJavaScriptを実行できるようになると、ローカルストレージに保管されているトークンを取得されてしまいます。

デメリット

  • 画面のリロードやタブの削除を行うと、JWTが消えるためログアウトされる
  • タブ間でログイン状態を保持できない
    →そのままだと、ユーザー認証を何度も行う必要があり、ユーザーの利便性がよくないが、リフレッシュトークンを併用することで、セキュリティを保ったまま、利便性を向上できます。

そもそもJWT認証は推奨されるのか?

Please Stop Using Local Storage - Randall Degges

If you need to store sensitive data, you should always use a server-side session

徳丸さんのスライド

WebサーバーとAPIサーバーが一体の場合は古典的なセッションを使うのが比較的無難 - セッション管理に由来する脆弱性は枯れていて十分対策されているため

クロスドメインでCookieを使うのは非常に難易度が高い
Cookieはクロスドメインで使わない方がよいと思います

以上のように、WebサーバーとAPIサーバーが一体で同一オリジンの場合は、従来のサーバーサイドセッションを使用するのが安全な方法と考えられます。ただ、異なるオリジン構成の場合は、CookieにsameSite: Noneを設定する必要があり、CSRF脆弱性への対策が別途求められるので、Cookieを使用することの難易度が高くなります。
このような場合には、JWT認証など他の認証方式を検討する余地があると理解しました。


また、徳丸さんのスライドでは以下のような記述もあります。

JWTのようなステートレス・トークンを使う場合は、そのリスクを検討した上で、必要に応じてAPIゲートウェイ等を検討する

JWTを使用する際のXSS以外のリスクとしては以下のようなものが存在します。

  1. トークンが流出した際にトークンを即時無効化できない
  2. 秘密鍵の変更でトークン流出には対応できそうだが、すべてのユーザーがログアウトされる

1のリスクは、アクセストークンの有効期限を短くしてリフレッシュトークンを使うことで対策できますが、結局はどの程度のリスクを許容できるかですね、、

以上を踏まえてどのような認証機能を実装したのか?

以下のようなトークン保存戦略を採用したJWT認証の実装を行いました。
 
まず、トークンの保存方法と有効期限を以下のように設定しました。

トークンの種類 保存方法 有効期限
アクセストークン(JWT) in-memory 15分
リフレッシュトークン(JWT) HttpOnly Cookie 30日

アクセストークンをin-momoryで、リフレッシュトークンをHttpOnly Cookieで保管することでXSSへの対策を行なっています。また、アクセストークンの有効期限を短く設定することでトークン流出時のリスクに備える一方、ユーザー体験向上のため有効期限の長いリフレッシュトークンを併用しています。
 
フロントエンド・バックエンドを別ドメインで運用していることから、CookieにsameSite:Noneを設定しているため、CSRF脆弱性が存在します。ただ、Cookieが送信されるのは、/tokenというアクセストークン再発行用のエンドポイントへのリクエストのみであり、サーバ側のCORS設定(Access-Control-Allow-Originヘッダ)により正規のユーザーのみがそのレスポンスを受け取ることができます。つまり、攻撃者はアクセストークンを取得することができず、Cookieを不正利用してできることも限られていることから、CSRFへのリスクを許容しています。

より堅牢にするには、CSRFトークンの導入が必要になりそうですが、上記の理由とプロジェクトが小規模であったことから、導入を見送りました。

最後に

ご一読いただきありがとうございました!

参考

徳丸さんのスライド
Please Stop Using Local Storage - Randall Degges
HTML5 Security Cheat Sheet - OWASP
トークンストレージ - auth0 Docs

いまさらLocal Storageとアクセストークンの保存場所の話について - Qiita
【PoC編】XSSへの耐性においてブラウザのメモリ空間方式はLocal Storage方式より安全か? - GMO Flatt Security Blog

Discussion

Ohkubo KOHEIOhkubo KOHEI

素晴らしいソリューションです、なんか名前をつけておきたいです。

shibainushibainu

ありがとうございます!!
今後も適宜アップデートしていければと思います!