Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

HTML5のLocal Storageを使ってはいけない(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。
画像は元記事からの引用です。

追記(2021/10/18)

Local Storageについては徳丸先生の以下の資料もおすすめです。Local Storageは使い方次第という観点で解説しています。

追記(2022/08/23)

以下の徳丸先生のツイートからリンクされている記事もどうぞ。

HTML5のLocal Storageを使ってはいけない(翻訳)

本気で申し上げます。local storageを使わないでください。

local storageにセッション情報を保存する開発者がこれほど多い理由について、私にはさっぱり見当がつきません。しかしどんな理由であれ、その手法は地上から消えてなくなってもらう必要がありますが、明らかに手に負えなくなりつつあります。

私は毎日のように、重要なユーザー情報をlocal storageに保存するWebサイトを新たに開いては頭を抱え、それをやらかして致命的なセキュリティ問題への扉を開いてしまう開発者がいかに多いかを思い知ってつらい気持ちになっています。

それでは、local storageとは何か、そしてlocal storageにセッションデータを保存してはならない理由について、私の魂の奥底の叫びをお伝えしたいと思います。

local storageとは

私の文章が以前よりトゲトゲしく感じられたら申し訳ありません。あなたを責めているわけではありません。この記事をお読みのあなたは、local storageをご存知でないかもしれませんし、既にセッション情報をlocal storageに保存してしまっている可能性もあるでしょう。

まずは基本を押さえましょう。local storageはHTML5の新機能であり、Webブラウザ(すなわちブラウザのユーザー)がJavaScriptを用いて任意の情報を保存できる仕組みです。単純な話ですね。

実用上のlocal storageは、昔ながらの巨大なJavaScriptオブジェクトであり、そこにデータをアタッチすることも削除することもできます。local storageに私の個人情報を少しばかり保存してから表示し、(必要なら)削除するJavaScriptコードの例をご覧ください。

// 以下のいずれかの方法でlocal storageにデータを保存できる
localStorage.userName = "rdegges";
localStorage.setItem("favoriteColor", "黒");

// localStorageにデータが入ると、明示的に削除するまで永久に残る
alert(localStorage.userName + "本当に好きな色は、" + localStorage.favoriteColor + "。");

// local storageからデータを削除してエントリを消すことも比較的簡単
// 以下のコメントを外すだけでできる
//localStorage.removeItem("userName");
//localStorage.removeItem("favoriteColor");

上のJavaScriptコードをブラウザのテスト用HTMLページで実行すると、「本当に好きな色は、黒です。」とalertメッセージに表示されます。次にDeveloper Toolsを開いてみると、userName変数とfavoriteColor変数のどちらもブラウザのlocal storageに保存されていることがわかります。

何らかの方法でlocal storageのデータをある時点で自動的に削除する方法があれば、入れた変数をいちいち手動で削除しなくてもよくなるのではないか?とお思いの方もいるかもしれません。ありがたいことに、頼もしいHTMLワーキンググループはsessionStorageと呼ばれているものをHTML5に追加してくれています。sessionStorageは、ユーザーがブラウザタブを閉じた瞬間に自動的に削除される点を除けば、local storageと「完全に同じ」です。

local storageの嬉しい点

local storageの話ついでに、local storageのクールな点についてもお話しておきましょう。本記事のねらいは、あくまでlocal storageにセッションデータを保存することをみなさんにやめてもらうことです。local storageには、他にも興味深い特性がいくつもあるのです。

まずlocal storageは「純粋なJavaScript」という点が最高です。local storageに代わる選択肢といえば現実にはcookieしかありませんが、cookieの残念な点の1つは「Webサーバーが作成する必要がある」ことです(´・ω・`)。Webサーバーでcookieを扱う作業は退屈かつ面倒です。

SPA(Single Page Application)などの何らかの静的サイトを構築しているとしましょう。そのサイトでlocal storage的な手段を使えれば、あらゆるWebサーバーから切り離された形でWebページを実行できるようになります。バックエンド側の言語やロジックを一切使わずに、思う存分ブラウザ側にデータを保存できるのです。

local storageの概念はかなり強力なので、local storageがWeb開発者の間でこれほど人気を勝ち得た大きな理由のひとつとなっています。

local storageには他にもうれしい点があります。最大サイズが4 KBに制限されているcookieに比べてサイズの制約がかなり緩く、主要なブラウザなら5 MBを超えるデータ・ストレージを楽々利用できます。

local storageは、特にアプリケーションのデータをブラウザにキャッシュして後で再利用したい場合に便利です。cookieの4 KB制約は苦しいので、local storageは現実的な代替手段のひとつとなっています。

local storageの残念な点

local storageの長所についてひととおり説明しましたので、今度はlocal storageの残念な点についても1分ほど(2分かかるかもしれませんが)お時間をいただきたく思います。

いいですか、local storageは、それはそれはびっくりするほど「初歩的」なのです(ああ、すっきりした😋)。local storageは、もうあきれるほど初歩的でシンプルなAPIです。

実際のlocal storageがどれほど初歩的か、たったこれだけのことが開発者にはほとんど知られていない気がしています。

  • local storageには「stringデータしか保存できません」👎。これでは単なるstringデータの範疇から少しでもはみ出したデータを保存できず、かなり不便です。やろうと思えば、データ型を含めて一切合財をシリアライズしてlocal storageに保存することもできなくはありませんが、相当えぐいハックになります。

  • local storageは「同期的」です。つまり、local storageへの操作は同時に1つしか実行できないのです。複雑なアプリケーションでlocal storageを使うと速度が落ちてしまい、使い物になりません。

  • local storageは「web workersから利用できません」😕。つまり、パフォーマンス向上のためにバックグラウンド処理やChrome拡張などを用いるアプリケーションを構築する場合、local storageはまったく利用できなくなります。理由は、local storageはweb workersでは使えないからです。

  • local storageに保存できるデータサイズには依然として上限があります(主要なブラウザでは最大5 MBまで)。大きなデータを扱うアプリケーションやオフラインで使うアプリケーションを構築するにはかなり厳しい制約です。

  • local storageはあらゆるJavaScriptコードから自由にアクセスできてしまいます(データ保護的なものはありません)。これはセキュリティ上非常に重大です(ここ数年で自分をイラつかせる問題ナンバーワンです)。

要するに、local storageを使うべきシチュエーションは次しかありません。「秘密情報を一切含まないこと」「一般に入手可能な情報であること」「そこそこの量である(5 MBを超えない)こと」「stringのみの情報であること」「保存するアプリケーションでパフォーマンスを要求されないこと」です。

自分が使うアプリケーションが上の条件を満たさないのであれば、local storageを使ってはいけません。他の何かをお使いください(後述)。

local storageが危険な理由と、そこに秘密データを保存してはならない理由

率直に申し上げます。local storageの残念な点は多々ありますが、そのすべてが重大というわけではありません。もちろんlocal storageを賢く使う分には構いませんが、アプリケーションの速度も少々落ちますし、開発上の軽いつらみもあります。しかしセキュリティとなると話は別です。local storageのセキュリティモデルは、何が何でも理解しておくことが重要です。さもないと、自分でも気づかないうちにWebサイトが重大な危機に陥ります。

local storageは一言で言うと「セキュアではありません」。これっぽっちもセキュアではないのです!秘密情報、たとえばセッションデータやユーザーの個人情報やクレジットカード情報、その他Facebookに一般公開で投稿したくないありとあらゆる情報を(たとえ一時的にであっても!)local storageに保存するのは大間違いです。

local storageの設計は、ブラウザでセキュアなストレージメカニズムとして用いる前提ではありませんでした。local storageは、少々複雑なSPAで開発者がstringのキーバリューストアとして用いるためだけに設計されたのです。

この世で最も危険な存在といえば何だと思いますか?そう、JavaScriptです。

考えてみてください。秘密情報をlocal storageに保存するということは、この世で最も危険なJavaScript様が史上最悪の地下金庫にあなたの大事な大事な秘密情報を保管しているのと本質的に変わらないのです。ベストな方法とは到底言えません。

この問題の行く先には、かのクロスサイトスクリプティング攻撃(XSS)が待ち構えています。本記事ではXSSについてくどくど説明しませんが、この部分は高度な話題となります。

攻撃者があなたのWebサイトでJavaScriptを実行すると、local storageに保存されているデータを残らず取り出して、攻撃者が所有するドメインに送信できてしまいます。つまり、local storageに保存された秘密情報(ユーザーのセッションデータなど)を悪用できてしまうということになります。

こういう話をすると、「それの何かマズいの?自分のWebサイトはセキュアだし、攻撃者がJavaScriptを実行できないようにしてあるし」とお考えの方がいらっしゃるかもしれません。

それはそれで一理あります。Webサイトが「真の意味で」セキュアで、攻撃者がWebサイトでJavaScriptコードを実行できないのであれば、技術的には安全です。しかし、これを現実に達成するのは非常に困難です。この点について解説します。

Webサイトに以下のような、外部ドメイン由来の「何らかの」第三者によるJavaScriptコードが含まれているとします。

  • bootstrapへのリンク
  • jQueryへのリンク
  • Vue、React、Angularなどへのリンク
  • その他広告ネットワークへのリンク
  • Googleアナリティクスへのリンク
  • その他のトラッキングコードへのリンク

このWebサイトには、攻撃者がJavaScriptを実行するリスクが存在します。たとえば、Webサイトに以下のscriptタグが埋め込まれているとしましょう。

<script src="https://awesomejslibrary.com/minified.js"></script>

ここでawesomejslibrary.comが乗っ取られてminified.jsスクリプトが以下のように改変されたとします。

  • local storageの全データをスキャンする
  • 盗み出した情報を収集用APIに送信する

これでWebサイトは一巻の終わりです。攻撃者がlocal storageのあらゆる情報を存分に悪用しても、誰ひとり気づかないままです。こんな状況がお望みですか。

私もエンジニアとして、自分たちのWebサイトには第三者のJavaScriptコードを埋め込むことを一切拒否したくなることもしょっちゅうです。しかしそれが通ることは現実にはめったにありません。

多くの企業では、マーケティングチームがさまざまなWYSIWYGエディタやツールを用いて一般公開のWebサイトを直接管理しています。そうした人たちが「自分たちのサイトには第三者のJavaScriptコードなどひとかけらもない」と自信を持って言い切れるものでしょうか?私なら「無理」と答えるでしょう。

第三者のJavaScriptコードが手違いで存在した場合にセキュリティリスクを大きく下げたければ、「local storageには決して秘密情報を保存しない」ことです。

重要なお知らせ: JSON Web Token(JWT)をlocal storageに保存してはいけません!

ここまで書いたことで、local storageに重要な情報を「決して」保存してはならないということが私にとっては明確になったと思いますが、特にJSON Web Token(JWT)についてここで言及しておく必要があると思います。

現在私が見たところ、セキュリティ上最大の脅威となるのはlocal storageにJWT(セッションデータ)を保存している人たちです。JWTがユーザー名とパスワードと本質的に同じであるという事実に、多くの人が気づいていません。

攻撃者がJWTをコピーすると、ユーザーがまったく気づかないうちにWebサイトへのリクエストをユーザーの代わりに実行できてしまいます。JWTはクレジットカードやパスワードと同じぐらい慎重に扱うものであり、間違ってもlocal storageには保存してはいけません。

恐ろしいことに、「JWTはlocal storageに保存して認証メカニズムに使いましょう」という誤った知識を教えているチュートリアルやYouTube動画やプログラミング教室や大学やブートキャンプが何千とあります。この情報は間違いです。こういうことを平気で教えるような場に居合わせたら、今すぐ逃げてください!

local storageの代わりに何を使えばいいか

local storageのあらゆる短所を考えると、何を代わりに使うべきでしょうか?代替手段を探ってみましょう。

重要なデータの保存

重要なデータを保存する必要があるなら、常にサーバーサイドセッションを使うべきです。以下は重要なデータの例です

  • ユーザーID
  • セッションID
  • JWT
  • 個人情報
  • クレジットカード情報
  • APIキー
  • その他、Facebookで人目にさらしたくないような情報すべて

重要なデータを保存する必要が生じた場合は、以下のようにします。

  • ユーザーがWebサイトにログインするときは、ユーザーのセッションIDを作成して、暗号化済み署名cookieに保存します。Webフレームワークを使っている方は、フレームワークガイドの「cookieでユーザーセッションを作成する方法」あたりの情報に従いましょう。

  • Webフレームワークで使うcookieライブラリは、種類を問わずhttpOnly cookieフラグを必ず設定してください。このフラグを設定することで、ブラウザ側で(訳注: JavaScriptを用いて)cookieを読み取ることは不可能になります。この設定は、cookieでサーバーサイドセッションを用いる場合の必須手順です。詳しくは勇敢なるJeff Atwoodの記事をご覧ください。

  • 使うcookieライブラリでは、SameSite=strict cookieフラグ(これはクロスサイトリクエストフォージェリ: CSRF 攻撃を防止します)と、secure=trueフラグ(cookieを暗号化済みコネクションでのみ設定できるようにします)も必ず設定してください

  • ユーザーがサイトにリクエストを送信するたびに、必ずセッションID(セッションIDはユーザーから送信されたcookieから取り出します)を使ってアカウントの詳細情報をデータベースまたはキャッシュから取り出してください(どちらを使うかはサイトの規模によります)。

  • ユーザーのアカウント情報を取り出して検証を終えたら、後はそこから関連する秘密情報を自由に取り出して利用します。

このパターンはシンプルかつ素直であり、最も重要な点は「セキュア」であるという点です。言うまでもなく、このパターンでほとんどの大規模なWebサイトをスケールアップできます。くれぐれも私の目の前で「JWTはステートレス」だの「JWTは速いぞ」だの「JWTはlocal storageに保存するよね」などと口走らないでください。それは間違っています!

string以外のデータ

重要ではないが純粋なstringではないデータをブラウザ側に保存する必要があるなら、IndexedDBがベストの選択です。IndexedDBならブラウザでデータベースらしいオブジェクトストアを使えます。

IndexedDBの素晴らしい点は、型情報(integer、floatなど)も保存できることです。主キーの設定やインデックス生成などの操作、トランザクションの作成も行えますし、データの一貫性も担保できます。

IndexedDBの概要と使い方のチュートリアルは、GoogleのIndexedDBチュートリアルがよくできています。

オフラインデータ

アプリケーションをオフラインで動かす必要があるなら、上述のIndexedDBとCache API(これはService Workerの一部です)の組み合わせがベストの選択です。

Cache APIを使えば、アプリケーションで読み込む必要のあるネットワーク上のリソースをキャッシュできます。

Cache APIの概要と使い方のチュートリアルについては、これもGoogleのCache APIチュートリアルがよくできています。

どうかlocal storageを使わないでください

今回local storageについて説明する機会を持つことができました。local storageを(おそらく)使うべきでない理由を皆さまにご理解いただければ幸いです。

以下に該当する、公開して構わない情報を保存する場合ならともかく、

  • 重要な情報を一切含まない
  • 極端なハイパフォーマンスを要求されない
  • 5 MBに収まる
  • stringデータだけの情報

それ以外の場合はlocal storageを使わないでください!正しいツールで仕事を進めましょう。

最後にお願いです。どうか、どうかJWTなどのセッション情報をlocal storageに保存しないでください。これは非常にマズい方法であり、極めて広範囲からの波状攻撃を許してしまい、顧客に損害を与えます。

ご質問はr@rdegges.comまでどうぞ。

皆さんがご無事でありますように。

原注

「XSSの影響はContent Security Policy(CSP)で緩和できる」点への言及がないことが気になる方へ: 本記事ではあえてCSPに言及しないことにしています。理由は、本記事を理解するうえでの助けにならないからです。たとえCSPを用いて第三者のJavaScriptドメインをすべてホワイトリスト化したところで、その第三者プロバイダそのものが乗っ取られてしまえばXSSを防ぎようがありません。

もうひとつ、サブリソース完全性はこれはこれでよいものですが、それでもXSS問題に対するグローバルな解決方法ではありません。広告ネットワークなどのマーケティングツール(=最も典型的な第三者JavaScript)でサブリソース完全性が用いられることは、まずありません。そうしたスクリプトの提供者は、ツールのユーザーにいちいち知らせずに無言で機能を更新する形で、スクリプトを頻繁に更新したがるものだからです。

原文追記

「local storageに重要なものを保存すべきではない」と考えているのは私だけではありません。OWASPも同意見です。

つまり、そのデータが保存されているコンピュータに対するローカル権限を持つユーザーであれば、アプリケーションで必須とされているあらゆる認証をバイパスできてしまいます。したがって、local storageには重要なデータを一切保存しないことをおすすめします。
cheatsheetseries.owasp.orgより大意

原文追伸

ここまでお読みいただいた方は、よろしければTwitterGitHubで私をフォロー、RSSを購読、または元記事末尾のフォームでの本ブログ購読をどうぞ(私が記事を追加したときにメールでお知らせします)。

追記(2019/11/30)

auth0.comでもLocal Storageにトークンを保存してはいけないというポリシーになっていますね。

参考: Where to Store Tokens

おたより発掘

徳丸先生ありがとうございます!こちらの本ですね↓。

関連記事

Railsアプリで実際にあった5つのセキュリティ問題と修正方法(翻訳)

Unicodeで絶対知っておくべきセキュリティ5つの注意(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。