history-state-form-preservation-research

History.state を用いたユーザ入力値の保存 ― 調査レポート

1. History.state でユーザ入力値を保存するライブラリ・フレームワーク

1.1 フレームワーク組み込み機能

Inertia.js ― 最も直接的な実装

Inertia.js は useRemember / useFormHistory.state にフォームデータを自動保存・復元 する機能を公式に備えている。

// Vue 3 例 — 自動保存
import { useRemember } from "@inertiajs/vue3";
const form = useRemember({ first_name: null, last_name: null });

// useForm にキーを渡すだけで自動保存 + バリデーションエラーも記憶
import { useForm } from "@inertiajs/vue3";
const form = useForm("CreateUser", data);                    // 静的キー
const form = useForm(`EditUser:${props.user.id}`, data);    // 動的キー

// 手動管理
import { router } from "@inertiajs/vue3";
router.remember(data, "my-key");   // 保存
let data = router.restore("my-key"); // 復元
項目 詳細
保存先 history.replaceState() — ブラウザ履歴エントリの state オブジェクト
復元トリガー popstate イベント(ブラウザの戻る/進むボタン)
対応フレームワーク Vue / React / Svelte
バックエンド Laravel / Rails / 任意
セキュリティ .dontRemember() でパスワード等の除外が可能
制限事項 ブラウザの replaceState() 呼び出し頻度制限あり。高頻度更新はデバウンス必須

出典:


React Router v6+ / Remix ― スクロール復元は実装済み、フォームは提案段階

React Router: <ScrollRestoration> でスクロール位置は History.state に保存済み。フォームデータの自動復元は v7 で future.flags として導入予定(現在 GitHub Discussion で議論中)。

// 提案されている実装イメージ(React Router v7)
// location.key ごとにフォームデータをメモリ + sessionStorage に保存
// <Form> マウント時に input.defaultValue !== input.value なら復元
// <Form restore="off"> でオプトアウト可能

Remix: ScrollRestoration コンポーネントは History.state ベースで実装済み。フォームデータは useFetcher + location.state で手動管理が必要。

出典:


Turbo (Hotwire)

Turbo Drive は History API でナビゲーションを管理。turbo:before-cache でフォーム状態をリセットしたり、Stimulus コントローラで pushState をカスタム実装するパターンが一般的。フォーム自動保存は組み込みではない

出典:


1.2 専用ライブラリ

ライブラリ 保存先 説明 出典
useHistoryState (カスタムフック) history.replaceState() React Router の useHistory を使用。useState の代わりに useHistoryState に置き換えるだけで全入力値が永続化 gal.hagever.com / CodeSandbox
react-form-autosave localStorage フォーム自動保存に特化。除外フィールド・デバウンス・Undo/Redo対応。History.state は不使用 GitHub
form-storage (appleple) localStorage フォームデータを自動保存。History.state は不使用 公式サイト
history.js (browserstate) History API フォールバック HTML4 ブラウザへの pushState/replaceState ポリフィル。主にSPAナビゲーション用 GitHub

1.3 セキュリティ観点での注意点

History.state にユーザ入力を保存する場合のリスク:

  1. XSS 時の影響: History.state は document スコープでアクセス可能。XSS 脆弱性がある場合、保存されたセンシティブデータが盗まれる
  2. データ容量制限: ブラウザによって pushState/replaceState の state サイズ制限が異なる。Firefox: 640KB(シリアライズ済み文字数制限、再起動後もディスクに保存するため)。Chrome: 仕様上の制限なし(1MB超で動作確認済み)。IE11: 約524KB。超過時は例外スロー
  3. ブラウザ履歴の閲覧: 開発者ツールで history.state は閲覧可能。機密データは保存しない
  4. タブ間隔離: History.state はタブごとに独立。localStorage のようなクロスタブ衝突の問題はない(利点)

2. History API の実装経緯と想定用途

2.1 背景 ― AJAX/SPA の台頭とブラウザ歴史管理の崩壊

2000年代後半、AJAX と Single-Page Application (SPA) が普及するにつれ、従来のブラウザ履歴管理では不十分な状況が生じた:

2.2 設計思想

HTML5 History API は以下の原則で設計された:

"URLs are historically important markers for resources. Make them meaningful and organised. Make sure you can directly access them without JavaScript. Only then should you add your JavaScript to enhance the browsing experience."
— HTML5 Doctor

"I don't think I can overstate the importance of the URL. URLs are permanent: users make favorites of them, search engines index them and companies market them."
— Clark Sell, Microsoft (MSDN Magazine, Aug 2012)

コアゴール:

  1. JavaScript アプリケーションがブラウザの戻る/進むボタンを正しく機能させる
  2. URL をクリーンなパス(/page/2)に変更可能にする(# なし)
  3. 各アプリケーション状態に対応する意味のある URL を維持する
  4. サーバー側でその URL を直接解釈可能にする(プログレッシブエンハンスメント)

2.3 W3C 仕様での定義

W3C HTML5 Working Draft (2011) で以下のように定義された:

出典:

2.4 ブラウザ実装の歴史

ブラウザ バージョン
Chrome 5+ 2010
Safari 5.0+ 2010
Firefox 4.0+ 2011
Opera 11.50+ 2011
iOS Safari 4+ 2010
IE 未サポート(polyfill 必要)

ポリフィル: History.js が HTML4 の hashchange + フラグメント識別子で API をエミュレート

2.5 仕様が想定していた本来の用途 vs 現状

本来の設計用途 現状の使われ方
SPA ルーティング(ページ遷移の履歴管理) ✅ 主要用途。React Router, Vue Router, Angular Router 等が標準採用
クリーンURLの維持(# なし) ✅ 標準的に使用
スクロール位置の保存 ✅ React Router / Remix の ScrollRestoration
アプリケーション状態の保存(アニメーション座標等) ⚠️ 一部で使用
フォーム入力値の保存 ⚠️ 仕様の本来の用途ではないが、Inertia.js 等で応用されている

仕様上、state オブジェクトは「URL に入れたくない一時的なドキュメント固有の状態」を想定していた。フォームデータ保存は明示的な用途ではないが、タブ間隔離という特性から Inertia.js のようなフレームワークが応用している。

3. なぜ Web Storage ではなく History.state を使うのか

Web Storage(localStorage / sessionStorage)でも技術的にはフォーム保存は可能だが、history.state を採用する理由は以下の通り。

3.1 タブ間隔離 (Tab Isolation)

保存先 タブ間で共有? 特徴
localStorage ✅ 共有される 同一オリジン内の全タブ・ウィンドウで共有。マルチタブで同時にフォームを開くとデータが衝突
sessionStorage ❌ タブごとに隔離 各タブで独立。ただし window.open() で開いたサブウィンドウは親のsessionStorageを共有する
history.state ❌ タブごとに隔離 各タブは独自の履歴スタックを持つ。さらに履歴エントリごとにも分離される

localStorage と異なり、sessionStoragehistory.state はどちらもタブ隔離されている。ただし sessionStorage は同一タブ内の全ページ遷移で共有されるのに対し、history.state は履歴エントリごとに分離される点が根本的に異なる。

3.2 履歴エントリとの自動紐付け

history.state の最大の利点は、状態が履歴エントリ自体に紐付くこと:

保存先 状態と履歴の関係 管理コスト
Web Storage URLや履歴エントリと関係なくキーバリューで保存 どのページにどの状態が対応するかを手動で管理する必要がある(location.key 等をキーにする工夫が必要)
history.state 状態が履歴エントリに直接含まれる ブラウザが「どの状態をいつ復元すべきか」を自動管理。戻る/進む時に popstate イベントで自動的に渡される

3.3 ライフサイクル

保存先 データが消えるタイミング フォーム保存への適合性
localStorage ユーザが手動で削除するまで残り続ける ❌ 不要なデータが永続的に残り、メモリ/容量を消費
sessionStorage タブを閉じるまで残り続ける ⚠️ 不要なナビゲーション後も残り続ける
history.state 履歴エントリが消える時(履歴の最大件数に達して追い出される、またはオリジン遷移) ✅ 必要な期間だけ保持して自動で消える

3.4 まとめ

観点 localStorage sessionStorage history.state
タブ間衝突 あり なし(タブ隔離) なし(タブ隔離)
履歴エントリごとの分離 なし なし(同一タブ内で共有) ✅ あり
戻る/進むとの連携 手動実装が必要 手動実装が必要 自動(popstate で復元)
キー管理 手動 手動(location.key 等をキーに) 不要(履歴エントリに自動紐付)
データ寿命 永続的 タブ閉じるまで 履歴エントリと同期して自動削除

Inertia.js の useRememberhistory.replaceState() を採用しているのは、履歴エントリごとの自動分離と戻る/進むとの自動連携による。


4. セキュリティ考察 — History.state に妥当な代替手段は存在するか

4.1 根本的なトレードオフ

history.state の3つの利点(タブ隔離・履歴エントリごとの状態・即時復元)は、全て「JavaScript からアクセス可能なクライアントサイド記憶領域」であることに依存している:

利点 なぜクライアントサイドが必要か
タブ間隔離 サーバーサイドはブラウザのタブを識別できない
履歴エントリごとの状態 ブラウザの履歴スタックはクライアント側のみが管理
即時復元(popstate) サーバー往復では復元にレイテンシが発生しUXが損なわれる

そして クライアントサイドの記憶領域は全て XSS に対して等しく脆弱である。history.statesessionStoragelocalStorage も、XSS が成立すれば攻撃者に読み取られる点は変わらない。

4.2 代替手段の検討

保存先 タブ隔離 履歴エントリごとの自動分離 XSS耐性
history.state
sessionStorage ❌(同一タブ内の全ページで共有)
localStorage
HttpOnly Cookie ✅(JSからアクセス不可)
サーバーサイドセッション ✅(SessionID必要) ✅(リクエスト毎に状態を管理)

sessionStorage はタブ隔離されているが、履歴エントリごとには分離されない。同一タブ内の全ページ遷移で共有されるため、「戻る」で前のページのフォーム状態を復元するには location.key 等をキーにした手動管理が必要。つまり history.state の「履歴エントリとの自動紐付け」を維持できるクライアントサイドの代替手段は 存在しない

サーバーサイドセッションや HttpOnly Cookie は XSS 耐性があるが、「即時復元」の UX とサーバー負荷のトレードオフが発生する。

4.3 結論

history.state の利点(タブ隔離 + 履歴自動紐付け)を享受しつつ、XSSリスクを排除する手段はない

取れる対策は根本的な代替ではなく、防御の多層化に限られる:

  1. 機密データを保存しない — Inertia.js の .dontRemember() がまさにこれ。パスワード、トークン、PII は除外する
  2. CSP (Content Security Policy) で XSS 自体の発生確率を下げる
  3. 入力値のサニタイズ — state に保存前にエスケープ
  4. サーバーサイドのフォーム復元 — 完全な別アプローチ(UX とサーバー負荷のトレードオフ)

まとめ: 「フォームデータを History.state に保存すべきではない」という認識は正しい。それをやるなら「機密データは絶対に含めない」が必須のラインとなる。Inertia.js 等がこれを採用しているのは、UX のメリットがセキュリティリスクを上回ると判断しているからであり、リスクがゼロになったわけではない。


2.6 主要リファレンス