はじめに
GMOコネクトの永田です。
「SPAのアクセストークン、localStorage に置いていいんでしたっけ?」と聞かれて、「ダメだった気がする」と答えかけて手が止まりました。自分の知識が数年前で止まっていたからです😇
確認したら、答えは変わっていました。OWASP ASVS は最新の 5.0 で、セッショントークンの localStorage 保存を条件付きで認めています。この一件をきっかけに 4.0→5.0 の差分まで追い、OIDC/OAuth の「RFC多すぎ問題」に ASVS が効くと分かった、という記録です。
ちょうど社内で SPA を共通IdP(Identity Provider=認証を担う基盤。Keycloak ベース)へ載せ替える設計レビュー中で、その実体験も交えます。
先にまとめ
- localStorage はもう一律NGではない: ASVS 5.0 の V14.3.3 (L2) がセッショントークンを例外として明記(4.0 の 3.2.3 は secure cookie/sessionStorage 限定)。ただし XSS対策・idle/absolute タイムアウト・ログアウト時クリアが前提。
- RFC横断の代わりに3章を当てる: 5.0 新設の V9(Self-contained Tokens)・V10(OAuth and OIDC)と、データ保護の V14。RP実装/レビューの物差しとして有効。
- ただし ASVS は万能ではない: セキュリティ統制の物差し。各チェックリストのスコープを理解し「ASVS+アーキレビュー+最小限の自作観点」を組み合わせる。
localStorageは結局アリなのか
最初の「localStorageはダメだった気がする」の出どころは、ASVS 4.0.3 の要件でした。4.0 系では、セッショントークンの保存先は secure cookie か HTML5 の sessionStorage に限る、という趣旨の記述で、localStorage は実質NG扱いだったわけです。
5.0 では、データ保護の章(V14)の該当要件がこう変わっています。
14.3.3 (Level 2): Verify that data stored in browser storage (such as localStorage, sessionStorage, IndexedDB, or cookies) does not contain sensitive data, with the exception of session tokens.
(ブラウザストレージ(localStorage / sessionStorage / IndexedDB / cookie)に機微データを置かないこと。ただしセッショントークンは例外とする。)出典: OWASP ASVS 5.0, V14 Data Protection(https://github.com/OWASP/ASVS/blob/master/5.0/en/0x23-V14-Data-Protection.md )
保存先として localStorage を名指しで除外するのをやめ、「機微データは置くな、ただしセッショントークンは例外」になりました。背景は Issue #2010 の議論で、「sessionStorage 一択はタブごとに分離されてマルチタブのUXを壊す。idle/absolute タイムアウトを併用すれば localStorage でも実害は変わらない」という整理です。(XSS対策をするのは大前提)
徳丸氏(@ockeghem)も「"ここが変だよASVS v4" が是正されていてよい」と評価しています。
ただしガイドラインによって見解が分かれる点は要注意です。プロジェクトが準拠するべきガイドラインや脅威モデリングに合わせて、適切なアーキテクチャを選定してください。
- ASVS 5.0(V14.3.3): 条件付きで容認
- IETF "OAuth 2.0 for Browser-Based Apps" draft / OWASP Session Management Cheat Sheet: in-memory + BFF(Backend for Frontend=フロント専用バックエンドにトークンを預ける構成)寄り
RFCは分散、ASVSは集約
OIDC/OAuth の実装レビューをやろうとすると、当てるべき仕様が大量にあります。ざっと挙げるだけでも RFC 6749 / 6750 / 7636(PKCE=認可コード横取り対策)/ 9068(JWT Access Token Profile)/ 9207(Mix-up=トークン発行元の取り違え対策)/ 8725(JWT BCP)/ 9700(OAuth Security BCP)… と続き、OIDC Core の §3.1.3.7(ID Token検証)あたりも見る必要があります。「これ全部、頭に入れてレビューするの?」というのが正直なところでした。
ここで効いたのが ASVS 5.0 です。4.0→5.0 で章構成が再編され、OIDC/OAuth とトークンが独立した章になりました。
| ASVS 4.0.3 | ASVS 5.0 | |
|---|---|---|
| OAuth/OIDC | 専用章なし(V3 Session等に分散) | V10 OAuth and OIDC として独立 |
| 自己完結トークン(JWT等) | 分散 | V9 Self-contained Tokens として独立 |
| Web フロントエンド | 分散 | V3 Web Frontend Security を新設 |
4.0→5.0 の差分はこれ以外にも広範ですが(章の再編やレベル要件の見直しなど)、全体像は OWASP 公式の移行ガイドにまとまっているので詳細はそちらに譲ります。本記事で効いたのは、OIDC/OAuth とトークンが独立章になった点です。
For Users of 4.0:
何が嬉しいかというと、OIDC/OAuth のRP(Relying Party=IdPに認証を委ねるアプリ側)を実装・レビューするとき、まず最初に見るべき一覧が V9 / V10 / V14 だと分かることです。散らばったRFCを横断で引く前のとっかかりになります。ASVS は要件ごとにRFC番号を振っているわけではありませんが、各章の末尾に関連仕様(RFC 6749 / 7636 / 9700、OIDC Core など)の References がまとまっています。「大量のRFCのどれを見ればいいか」を絞り込む入口として使えます。
実際のRP移行レビューでASVSが効いた
ちょうど社内で、ある SPA を内部IdPから共通IdP(どちらもKeycloakベース)へ載せ替える設計レビューをやっていました。SPAは public client(シークレットを安全に秘匿できない公開クライアント)の RP、バックエンドは Resource Server(トークンを検証してAPIを守る側)という構成です。
レビューで挙がった指摘を後から ASVS にマッピングしてみたら、セキュリティ系の指摘はほぼ V9 / V10 / V14 のどれかに乗りました。
| 指摘の内容 | ASVS 5.0 |
|---|---|
| Resource Server側の aud / iss / alg / JWKS 検証(受け手・発行者・署名アルゴリズム・署名鍵の検証)が未定義(Keycloakのデフォルト aud=account だと他RPのトークンが通りうる) | V9.1.2 / 9.1.3 / 9.2.3 / V10.3.1 |
| AT/RT(アクセス/リフレッシュトークン)を localStorage 保存 | V14.3.3(容認、ただしアクセストークンの短命化・セッションタイムアウト等が条件) |
| id_token を未検証で破棄・nonce(IDトークンの再利用を防ぐ値)不在・ログアウトの id_token_hint 不整合 | V10.5.1 / 10.5.4 |
| Mix-up攻撃(iss による発行元検証) | V10.2.2 |
| リフレッシュトークンのrotation・ロスト応答時の挙動 | V10.4.5 |
| sub 以外の可変なクレームをユーザー識別キーにしている | V10.3.3(不変の識別子 sub を使う) |
「都度こまかい観点を指定しなくても、ASVS の章を順に当てれば近い網羅性が出るのでは?」という感触になりました。そこで、これを検証してみました。
ASVSをブラインドレビューに使ってみた
Claudeのサブエージェントに、ASVS 5.0 の V9 / V10 / V14 と設計書だけを渡し、こちらの既存レビュー結果は一切見せずに設計レビューさせました。ASVS単体でどこまで指摘が再現するかを見るためです。
結果は、思っていたより良かったです。
自力で再発見した指摘:
- aud検証の不備(→ V9.2.3, V10.3.1)
- JWKS / iss / alg・鍵ソースの検証(→ V9.1.2, V9.1.3)
- id_token 未検証と nonce不在(→ V10.5.1, V10.5.4)
- Mix-up対策(→ V10.2.2)
- リフレッシュトークンのrotation(→ V10.4.5)
- sub 以外の可変なクレームをユーザー識別キーにしている問題(→ V10.3.3)
- localStorage保存(→ V14.3.3。容認だがアクセストークンの短命化・タイムアウト等が条件、と正しく条件付き判定)
こちらが見落としていたのに、ASVS側が拾った指摘:
- localStorage に PII(メールアドレスや氏名)まで載っていた(V14.3.3 はセッショントークンを例外にしただけで、PIIは依然NG)
- anti-cacheヘッダの不足(V14.3.2)
- 設定ファイルに confidential client(シークレットを秘匿できる機密クライアント)の secret が平文でコミットされていた(V10.4.4。旧内部Keycloakで使っていた廃止対象の設定だが、コミット履歴に残るのでローテーション対象)
セキュリティ統制の有無については、こまかく観点を指定しなくても、ASVS の章を順に当てるだけで主要な指摘が再現し、しかもこちらの見落としまで拾いました。RFCを暗記していなくても、ASVS を物差しにすればここまで来る、というのは収穫でした。
ただし ASVS はセキュリティ特化 — 組み合わせて使う
一方で、ブラインドレビューが取りこぼした指摘もありました。たとえば state と code_verifier(PKCE)をオンメモリ(JS変数)にしか持っていない という指摘です。認可エンドポイントへはトップレベルのリダイレクトで飛ぶため、ページ遷移でJS変数は揮発し、戻ってきたコールバックで照合もトークン交換もできません。sessionStorage 等、遷移をまたいで残る場所に置く必要があります。
ここがポイントなのですが、ASVS には「PKCE や state を使っているか」を問う要件はあっても、「その値が遷移をまたいで実際に残るか」を問う要件はありません。だからブラインドレビューは、PKCE も state も設計上は使われていることを根拠に「満たしている(SATISFIED)」と判定しました。これは ASVS の欠陥ではなく想定どおりの挙動です。守備範囲は「セキュリティ統制があるか」であって、その実装が正しく動くか(=実装の正しさ・アーキテクチャの妥当性)はスコープ外。OWASP はセキュリティに特化した標準なので、当然そうなります。
なので「ASVS が拾わない=チェックリストは実装の正しさを見られない」ではなく、各チェックリストには守備範囲があり、目的に応じて組み合わせるのが正解です。この指摘も、保存場所の観点を含むRP実装チェックリストやアーキテクチャレビューを足せば拾えます。ただし自作チェックリストを肥大化させるのは筋が悪い(将来のメンテコストがそのまま乗る)ので、標準に寄せられる部分は ASVS に寄せ、スコープから外れる観点(実装の正しさ・UX等)だけを最小限、自作側に残すのが現実的でした。
とはいえ実装ディテールが常にセキュリティ無関係なわけでもなく(例:再ログイン経路で state と PKCE が落ちる)、線引きは指摘ごとに見極めるのが実際のところです。
まとめ
- localStorage 容認、知識をアップデート: ASVS 5.0(V14.3.3)で条件付き容認に転換。「昔ダメだった」で止めない。前提は XSS対策・idle/absolute タイムアウト・ログアウト時クリア。
- ガイドラインは判断が分かれる、だから判断を文書化: ASVS は容認、Browser-Based Apps draft / Session Mgmt Cheat Sheet は in-memory/BFF 寄り。自分の脅威モデルでどちらを採るか決め、根拠を残す(5.0 自体が「決定の文書化」を要件化)。
- RP実装/レビューの入口は V9 / V10 / V14: RFC横断の前にこの3章。ブラインドレビューでも主要なセキュリティ指摘が再現し、こちらの見落としまで拾った。
- 単一チェックリストを万能視しない: 各チェックリスト/レビューのスコープを把握し、ASVS(セキュリティ)+アーキレビュー+最小限の自作観点を組み合わせる。実装の正しさ(オンメモリでフローが壊れる類)は ASVS のスコープ外。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。