【フロントエンド向け】JWTの安全な保管場所とCSRF・XSS対策の技術解説
はじめに
Webアプリケーションで広く利用されるJWTなどの認証トークンですが、その保管場所はセキュリティを大きく左右します。この記事では、localStorage
を利用する際のリスクを解説し、HttpOnly
属性付きクッキーとCSRFトークンを組み合わせた、堅牢なセキュリティ対策のロジックを技術的に解説します。
localStorage
は危険なのか (XSSの脅威)
1. なぜlocalStorage
へのトークン保存は実装が容易なため広く使われますが、XSS(クロスサイト・スクリプティング)攻撃に対して脆弱です。
-
XSS攻撃の概要: 攻撃者がウェブサイトの脆弱性を利用して、悪意のあるJavaScriptコードを他のユーザーのブラウザ上で実行させる攻撃手法です。入力フォームやコメント欄などが主な注入経路となります。
-
localStorage
のリスク:localStorage
に保存されたデータは、localStorage.getItem()
のようなJavaScriptコードで容易にアクセスできます。もしサイトにXSS脆弱性が存在すると、注入されたスクリプトによって認証トークンが読み取られ、攻撃者のサーバーに送信されてしまいます。トークンが漏洩すると、攻撃者はユーザーになりすましてAPIを不正に利用することが可能になります(セッションハイジャック)。
HttpOnly
属性付きクッキー
2. XSSの緩和策としてのHttpOnly
属性は、サーバーがクッキーを設定する際にこの属性を付与すると、ブラウザはそのクッキーへのdocument.cookie
などを介したJavaScriptからのアクセスを全面的に禁止します。
-
XSSへの効果と限界:
HttpOnly
属性により、たとえアプリケーションにXSS脆弱性が存在し、悪意のあるスクリプトが実行されたとしても、認証トークンの値自体を盗み出すことはできなくなります。トークンはブラウザがサーバーとの通信時に自動的にHTTPリクエストヘッダーに含めるだけで、スクリプトからはその存在を関知できません。
ただし、これはあくまでCookieの値を読み取れなくするだけで、XSS脆弱性そのものを解決するものではありません。サイトにXSS脆弱性が存在する場合、攻撃者のスクリプトはCookieを盗まずとも認証済みのユーザーとしてAPIリクエストを自由に発行できてしまいます。したがって、HttpOnly
は被害を軽減する緩和策と位置づけ、XSS脆弱性自体を作らない(入力値のサニタイズや出力時のエスケープ)ことが最も重要です。
3. 新たな脅威「CSRF」の登場
HttpOnly
クッキーはXSSによるCookie窃取に対しては有効ですが、別の種類の攻撃であるCSRF(クロスサイト・リクエスト・フォージェリ)に対しては無力です。
-
CSRF攻撃の概要: ログイン済みのユーザーを悪意のある第三者のサイトに誘導し、ユーザーの意図しないリクエストを正規のサーバーに強制的に送信させる攻撃手法です。「リクエストの偽造」とも呼ばれます。
-
なぜ
HttpOnly
クッキーだけでは防げないのか: クッキーは、そのドメインへのリクエストであれば、どのサイトから送信されたリクエストであってもブラウザによって自動的に添付されます。攻撃者はこの仕様を悪用します。罠サイトに設置されたフォームやスクリプトが正規サイトへのリクエストを送信すると、ブラウザは認証情報(HttpOnly
クッキー)を付けて送ってしまいます。サーバーは有効なクッキーが付与されたリクエストを受け取るため、それを正規のユーザーからの意図した操作であると誤認し、処理してしまいます。
4. 最終解決策:CSRFトークンでリクエストの正当性を証明する
CSRF攻撃を防ぐには、そのリクエストが正規のサイトから、ユーザー自身の操作によって送信されたものであることをサーバーが検証する仕組みが必要です。そのためにCSRFトークンを利用します。
-
CSRFトークンの仕組み:
- サーバーは、ユーザーがフォームなどを含むページを要求した際に、暗号学的に安全で予測不可能な文字列(CSRFトークン)を生成します。
- サーバーは生成したトークンをサーバーサイドのセッションに保存すると同時に、クライアントに返すHTMLの隠しフィールド(
<input type="hidden">
)などに埋め込みます。 - ユーザーがフォームを送信すると、そのリクエストにはフォームデータと共にHTMLに埋め込まれていたCSRFトークンが含まれます。
- リクエストを受け取ったサーバーは、リクエストに含まれるトークンと、自身のセッションに保存しておいたトークンを比較します。
- 両者が一致した場合のみ、リクエストを正当なものとして処理します。一致しない、またはトークンが存在しない場合は、不正なリクエストとして拒否します。
-
なぜCSRFを防げるのか: 攻撃者のサイトは、ブラウザの**同一オリジンポリシー(Same-Origin Policy)**により、正規サイトのHTMLコンテンツを読み取ることができません。そのため、HTMLに埋め込まれている正規のCSRFトークンを知ることができず、リクエストに含めることができません。結果として、攻撃者による偽造リクエストはサーバー側での検証に失敗します。
5. なぜCSRFはCORSで防げないのか?
「別サイトからのリクエスト」と聞くとCORSを連想しますが、古典的なCSRF攻撃はCORSのチェックを回避するように作られています。
-
「単純リクエスト」の存在: ブラウザは、互換性のために特定の条件(
GET
/POST
メソッド、標準的なContent-Type
など)を満たすリクエストを「単純リクエスト」として扱います。この種類のリクエストは、CORSの事前確認(プリフライトリクエスト)なしで直接サーバーに送信されます。 -
CSRF攻撃との関係: 歴史的に、CORSが策定されるより前からCSRF攻撃は存在していました。そのためCORSの仕様は、既存のHTMLフォームなどから送信される「単純リクエスト」については、「アプリケーション側で既に対策されているはず」という前提のもと、意図的にプリフライトチェックの対象外としました。このため、「単純リクエスト」を利用する古典的なCSRF攻撃は、CORSのプリフライトチェックでは防がれません。
Content-Type: application/json
などを使う現代的なAPIはプリフライトの対象となるためCSRF耐性が高まりますが、CSRFトークンによる対策は依然として不可欠です。
まとめ:推奨される多層防御のアプローチ
XSSとCSRFの両方からアプリケーションを保護するための現在のベストプラクティスは、複数の防御策を組み合わせる多層防御の考え方です。
保管場所/対策 | XSS対策 | CSRF対策 | 評価 |
---|---|---|---|
localStorage |
❌ 脆弱 | (直接関係なし) | 非推奨 |
Cookie (HttpOnlyあり) |
△ 緩和(窃取防止) | ❌ 脆弱 | 不十分 |
Cookie (HttpOnly) + CSRFトークン |
△ 緩和 + ○ 本質的対策 | ✅ 安全 | 推奨 |
※XSS対策の「本質的対策」は、XSS脆弱性自体を作り込まない(入力サニタイズ・出力エスケープ)ことです。
重要な認証トークンは**HttpOnly
属性およびSameSite
属性**(Lax
またはStrict
)を設定したクッキーに保存し、状態を変更するすべてのリクエスト(POST, PUT, DELETEなど)に対してCSRFトークンによる検証を実装することが、堅牢なウェブアプリケーションを構築する上での標準的なアプローチです。
参考文献 (References)
この記事で解説したセキュリティの概念は、以下の信頼できる情報源に基づいています。より深く学習したい場合の参考にしてください。
OWASP (Open Web Application Security Project)
Webアプリケーションセキュリティの標準的な情報を提供する世界的な非営利団体です。
-
Cross-Site Scripting (XSS) Prevention Cheat Sheet
- XSSの仕組みと、具体的な対策方法について詳細に解説されています。
-
Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
- CSRFの脆弱性の詳細と、CSRFトークンをはじめとする様々な防御策がまとめられています。
MDN Web Docs (Mozilla)
Web技術に関する公式ドキュメント。ブラウザの動作やAPIの仕様について最も信頼できる情報源の一つです。
-
HTTP クッキー
-
HttpOnly
属性やSameSite
属性を含む、クッキーの仕様について網羅的に解説されています。
-
-
オリジン間リソース共有 (CORS)
- CORSの仕組みや、プリフライトリクエストについて詳しく学ぶことができます。
-
サーバーサイドのウェブサイトプログラミングにおけるセキュリティの第一歩
- XSSやCSRFを含む、ウェブセキュリティの基本的な脅威について広く解説されています。
Discussion
『CSRF攻撃は、この「単純リクエスト」の仕組みを悪用します』と言う表現はおかしいと思います。元々CORSより先にCSRF攻撃の脅威があり、アプリケーション側の対策が必要でした。CORSの仕様策定時に、従来のHTMLフォームから送信可能なリクエストについては「CSRF対策がなされているはず」という前提の元にシンプルリクエストはプリフライトリクエストのチェックを不要となりました。このため、「単純リクエストについてはプリフライトリクエストによる保護は適用されない」なら正しいですが、「単純リクエストの仕組みを悪用」はおかしいと思います。
また、HttpOnlyつきのCookieのXSS防御を過剰に評価しているように思います。HttpOnlyなCookieであってもご指摘のように「クッキーは、そのドメインへのリクエストであれば、どのサイトから送信されたリクエストであってもブラウザによって自動的に添付され」るため、XSSのスクリプトからのAPI呼び出しではCookieが付与され、認証状態でAPIを悪用することができます。Cookie自体は乱数文字列などでありそれ自体に価値があるわけではなく、CookieつきでAPIを呼び出した結果得られる個人情報やさまざまな操作が守るべき本体ですが、HttpOnlyつきのCookieでこれらが守られるわけではありません。以下の動画も参考になるかと思います。
修正を確認しました。最終的な評価については異論があります。結局localStorageとHttpOnlyつきCookieは一長一短があるということに過ぎず、簡単に優劣をつけられるものではありません。
ockeghem様
非常に重要なご指摘、ありがとうございます!
特にHttpOnly属性の効果を過大評価していた点については、ご指摘の通り、読者に誤った安心感を与えかねない危険な記述でした。HttpOnlyはCookie値の窃取を防ぐ緩和策に過ぎず、XSS脆弱性がある状況ではAPIの不正利用は防げないという本質的な点を、明確にすべきでした。
ご指摘を受け、HttpOnlyを「緩和策」と明確に位置づけると共に、CORSとCSRFの歴史的経緯に関する記述も修正いたしました。
記事の信頼性を大きく高めるご指摘、ありがとうござます!大変勉強になりました!
上記は手違いで消去した返信です
改めまして
再度のご指摘、誠にありがとうございます。
おっしゃる通り、単純にどちらかが優れていると断定するのではなく、それぞれの技術が持つメリット・デメリットを理解することが重要だという点、大変勉強になりました。
どの方法を選択するにせよ、XSS対策やCSRF対策といった複数の防御策を組み合わせてセキュリティを高めていく必要がある、と改めて認識いたしました。
貴重なご意見をいただき、ありがとうございます。
徳丸先生の二番煎じですが、要は「XSS に脆弱でも HttpOnly 属性が付いていれば安全」ではなく「XSS に脆弱な時点でアウト」です。
もっと言えば「XSS に脆弱でも安全にするための手段」を考えることは無駄です。Cookie における HttpOnly 属性はその一つなので、HttpOnly 属性は本質的な対策としては無意味です。(HttpOnly 属性は本当に無意味なのか、何のために HttpOnly 属性が導入されたかについては考えてみてください)
XSS に脆弱にならないように対策することを考えなければなりません。
debiru様
的確なご指摘、ありがとうございます。
ご指摘の通り、「XSSに脆弱な時点でアウト」であり、HttpOnlyは本質的な対策ではなく、あくまで緩和策であるという点を明確にすべきでした。
その観点から、記事全体を見直し、修正いたしました。
記事の質を高める有益なフィードバックありがとうございます!