• 診断項目 No.73/127
  • 認証・アクセス制御
  • 重大度 最重要(critical)

クライアント保存値を認証判定に使っていないか?sessionStorage・localStorage認証の危険性・診断基準・対策

クライアント保存値を認証判定に使っていないかは、sessionStorageやlocalStorageの値だけで「ログイン済み」とみなす危険な実装を見分ける診断項目です。突破される仕組み、判定条件、PHP・nginx・Next.jsでの直し方を解説します。

結論(クライアント保存値を認証判定に使っていない)

クライアント保存値を認証判定に使っていないかとは、ブラウザのsessionStorageやlocalStorageに保存した「ログイン済み」「トークン」などの値の有無だけで、保護ページの表示や遷移を許可していないかを確かめる診断項目です。Web Storageの値は利用者が開発者ツールで自由に書き換えられるため、認証の根拠にはなりません。認証と認可は必ずサーバー側のセッションで検証します。

この記事のポイント

  • sessionStorage・localStorageは同一オリジンのJavaScriptから自由に読み書きでき、HttpOnlyのような保護属性がありません。
  • コンソールで sessionStorage.setItem('auth','1') と入力するだけで値を作れるため、「値がある=認証済み」は境界になりません。
  • トークンを保持してAPIへ送り、サーバーが検証する設計自体は直ちに違反ではありません(ASVS V14.3.3の例外)。問題は保存値を認可の根拠にすることです。
  • セキュリティAIチェッカーは、トップページHTMLで認証・トークン系の名前のキーをgetItem/setItemしている兆候を△(要確認)と判定します。
  • 対策は、サーバーで認証してHttpOnly・Secure・SameSite付きCookieのセッションで保持し、保護コンテンツはセッション検証後にのみ返すことです。
診断項目名クライアント保存値を認証判定に使っていない
項目IDauth.client-storage-signal
分類認証・アクセス制御
重大度・配点の重み最重要(critical) 重み 5
関連する標準・ガイドラインOWASP ASVS 5.0.0 V7.2.1(セッショントークンの検証は信頼できるバックエンドで行う)、V8.3.1(認可は信頼できるサービス層で強制し、クライアント側JavaScriptのように利用者が操作できる制御に依存しない)、V14.3.3(ブラウザストレージに機微データを置かない。ただしセッショントークンは例外)/OWASP WSTG-CLNT-12(Testing Browser Storage)、WSTG-ATHN-04(Bypassing Authentication Schema)/CWE-602(Client-Side Enforcement of Server-Side Security)、CWE-922(Insecure Storage of Sensitive Information)/IPA「安全なウェブサイトの作り方」1.4 セッション管理の不備・1.11 アクセス制御や認可制御の欠落

クライアント保存値を認証判定に使っていないかとは

クライアント保存値を認証判定に使っていないかとは、ブラウザの sessionStorage や localStorage(Web Storage)に「ログイン済み」「トークン」「ユーザー名」などを保存し、その値の有無だけで画面表示や遷移を許可する実装になっていないかを確かめる診断項目です。

// 危険な例:値があれば「ログイン済み」とみなしている
if (sessionStorage.getItem('loginFlag') !== 'ok') location.href = 'login.htm';

このような判定は、利用者自身がブラウザの開発者ツールで値を書き換えるだけで突破できます。セキュリティAIチェッカーは、トップページのHTMLに含まれるスクリプトから、認証・トークン系の名前を持つキーを Web Storage で読み書きしている兆候を検出し、「クライアント保存値を認証の根拠にしていないか」を要確認として知らせます。

自治体サイトでは、職員向けの簡易ポータル、議会資料の限定公開ページ、外部委託で作られた会員向けページなどで、「パスワードをJavaScriptで照合し、成功したら sessionStorage.setItem('auth','ok') を保存して次ページへ」という作りが見つかることがあります。この方式では、保護したつもりのページそのものがサーバー側で無防備に配信されているため、実質的に公開状態と変わりません。

Web Storage(sessionStorage・localStorage)の性質

  • localStorage はオリジン単位で永続し、sessionStorage はタブ(トップレベル閲覧コンテキスト)単位でタブを閉じるまで保持されます。
  • どちらも同一オリジンのJavaScriptから自由に読み書きでき、Cookieの HttpOnly のような保護属性はありません。
  • OWASP HTML5 Security Cheat Sheet は、セッション識別子を Local Storage に保存しないこと、単一のXSSでこれらのオブジェクト内の全データが盗まれ得ることを明記しています。
  • 利用者は開発者ツールのコンソールで sessionStorage.setItem('auth','1') と入力するだけで任意の値を作れます。したがって「値が存在する=認証済み」という判定はセキュリティ境界になりません。

クライアント保存値で認証してはいけない理由(保存と判定の違い)

「保存すること」と「判定に使うこと」は別の問題

OWASP ASVS 5.0.0 の V14.3.3 は、ブラウザストレージに機微データを置かないことを求めつつ、セッショントークンを例外として認めています。つまり、SPA がアクセストークンを sessionStorage に保持し、API 呼び出し時にヘッダーで送ること自体は直ちに違反ではありません。問題になるのは次のような使い方です。

  1. 保存値の有無・内容だけで保護コンテンツを表示する(サーバーは無条件にコンテンツを返す)。
  2. 保存値をサーバーへ送っても、サーバーがその値を検証せず信用する。
  3. 長期の localStorage に、失効させられないトークンや個人情報を置く。

要件の根拠(OWASP ASVS 5.0.0)

要件内容
V7.2.1すべてのセッショントークン検証を信頼できるバックエンドで行う
V8.3.1認可は信頼できるサービス層で強制し、クライアント側 JavaScript のような利用者が操作できる制御に依存しない
V14.3.3ブラウザストレージに機微データを置かない(セッショントークンは例外)

この診断項目は、これらの原則に反する実装の入口を探すものです。CWE-602(サーバー側のセキュリティをクライアント側で強制する弱点)、CWE-922(機微情報の安全でない保存)にも対応します。

よくある誤解

  • 「難読化したから大丈夫」:キー名や値を難読化しても、ブラウザ内で照合している以上、処理を追えば再現できます。
  • 「sessionStorage はタブを閉じれば消えるので安全」:消える時期の問題であり、改ざん耐性はありません。
  • 「パスワード照合をサーバーで行っているから安全」:照合結果をクライアント保存値として持ち、以後の保護ページ表示がその値の確認だけに依存しているなら、照合後の保護は無いのと同じです。

似た仕組みとして、遷移元URLで閲覧可否を決める実装は「Refererをアクセス制御に使っていない」、ログイン後に静的ファイルへ遷移させる構成は「認証後コンテンツを静的ファイルで直接配信していない」の各項目で確認します。

クライアント保存値を認証判定に使っていないの診断基準(セキュリティAIチェッカー)

取得対象

  • 診断対象URL(リダイレクト後の最終URL)のトップページHTML 1件(最大約2MB)。インライン <script> を含むHTML本文のみを対象とし、外部JavaScriptファイルは取得しません。
  • 追加の通信は発生しません(取得済みHTMLの文字列検査)。

判定に使う正規表現

次の正規表現(大文字小文字を区別しない)がHTML中に一致するかを調べます。

(?:sessionStorage|localStorage)\s*\.\s*(?:getItem|setItem)\s*\(\s*["'][^"']*(?:auth|token|session|login|user)

getItem/setItem の第1引数(キー名の文字列リテラル)に auth・token・session・login・user のいずれかが含まれると一致します。

判定条件

判定条件根拠表示
○ 問題なし(pass)一致なし取得HTML内に認証用途のクライアント保存値は確認されません。
△ 要確認(warn)上記パターンに一致認証・トークン系キーをsessionStorage/localStorageで参照する兆候があります。
× 要改善(fail)この項目では使用しない―
対象外(na)この項目では使用しない―

×(fail)にしない理由は、静的検査では「保存値を認可の根拠にしているのか」「単にトークンを保持して API へ送り、サーバーが検証しているのか」を区別できないためです。△は「要確認」であり、実際の認可の有無は下記の手動確認で確定させます。

得点への影響

重み5の critical 項目です。○は5点、△は 5×0.45=2.25点、×は0点として、対象外を除く全項目の重み合計に対する獲得割合が総合スコアになります。critical 項目のため、△が1件でも総合スコアへの影響は小さくありません。

自治体サイトでの判定例

HTMLの記述判定対応
if (sessionStorage.getItem('loginFlag') !== 'ok') location.href = 'login.htm';△保護対象ページを Cookie なしで直接取得し、内容が返るかを確認(「認証後静的ファイルの未認証到達性」)
文字サイズ変更機能の localStorage.setItem('fontSize', ...)○キー名に対象語が無いため対応不要
アクセス解析の localStorage.setItem('user_visit', ...)△になり得る対象語 user を含むため一致。認証用途でないことを確認すれば対応不要

検出の限界

  • 外部JSファイル内の処理、localStorage['token'] のようなブラケット記法、localStorage.token のようなプロパティ記法、IndexedDB は検出対象外です。
  • ○は「トップページHTMLに兆候が無い」ことのみを意味し、下層ページやログイン画面の実装を保証しません。

手動確認の手順

  1. ブラウザの開発者ツール →「アプリケーション」→ Storage で、保存されているキーと値を確認する。
  2. 保護されているはずのページURLを、別ブラウザ(またはシークレットウィンドウ)で直接開く。
  3. 次のように Cookie なしで取得し、200で本文が返るなら認可が欠落しています。
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.lg.jp/member/menu.htm

クライアント保存値で認証しているとどんなリスクがあるか

攻撃シナリオ

  1. 攻撃者がログイン画面のソースを読み、成功時に sessionStorage.setItem('auth','ok') してから menu.htm へ遷移することを把握する。
  2. コンソールで同じ値を書き込むか、そもそも menu.htm を直接開く。
  3. サーバーは無条件に menu.htm を返すため、職員向け資料・名簿・内部連絡などが閲覧される。

また、トークンを localStorage に置く設計では、XSSが一つでもあると全トークンが読み出され、別端末からのなりすましに使われます。HttpOnly Cookie なら JavaScript から読めないため、被害範囲を小さくできます。

想定される被害

  • 非公開資料・個人情報の漏えい(個人情報保護法上の漏えい等報告の対象になり得る)
  • 管理機能の不正利用、掲載情報の改ざん
  • 「パスワードで保護している」という誤った前提による発見の遅れ

クライアント保存値に頼らない認証の実装方法(PHP・nginx・IIS・Next.js)

基本方針

  1. 認証は必ずサーバーで行い、結果はサーバー側セッション(HttpOnly・Secure・SameSite 属性付き Cookie で紐付け)として保持する。
  2. 保護コンテンツは、サーバー側でセッションを検証してから返す経路(PHP・アプリサーバー・リバースプロキシの認証)でのみ配信する。
  3. クライアント保存値は「表示の都合」(ログイン中の名前表示など)に限定し、認可の根拠にしない。

PHP(共有ホスティング)

保護ページの先頭で読み込むガードです。

<?php
// member/guard.php — 保護ページの先頭で require する
session_set_cookie_params([
  'lifetime' => 0, 'path' => '/', 'secure' => true,
  'httponly' => true, 'samesite' => 'Lax',
]);
session_start();
if (empty($_SESSION['staff_id'])) {
  http_response_code(302);
  header('Location: /member/login.php');
  exit;
}
header('Cache-Control: no-store');

ログイン処理では、照合に成功したらセッションIDを再生成してから利用者IDを保存します(セッション固定化対策)。

<?php
// member/login.php(抜粋)— 照合成功時
session_regenerate_id(true);           // セッション固定化対策
$_SESSION['staff_id'] = $staffId;
header('Location: /member/menu.php');
exit;

静的な menu.htm は menu.php に置き換え、先頭で guard.php を読み込みます。旧 .htm は削除するか、次のように直接アクセスを拒否します。

# member/.htaccess
<FilesMatch "\.(?:html?|txt|csv)$">
  Require all denied
</FilesMatch>

nginx(認証サブリクエスト)

location /member/ {
    auth_request /auth/check;      # アプリ側でセッションを検証し 2xx/401 を返す
    error_page 401 = /member/login;
}
location = /member/login {
    auth_request off;              # ログイン画面自体は認証対象から外す
    proxy_pass http://app;
}
location = /auth/check {
    internal;
    proxy_pass http://app/auth/check;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

IIS

静的ページを保護する場合は「Windows 認証」または ASP.NET のフォーム認証を構成し、統合パイプラインで静的ファイルにも認証モジュールが適用されるようにします(<modules runAllManagedModulesForAllRequests="true" /> などの設定可否は環境に応じて確認します)。

Next.js(Vercel)

// proxy.ts(Next.js 16 では middleware の後継)
import { NextResponse, type NextRequest } from "next/server";
export function proxy(req: NextRequest) {
  const sid = req.cookies.get("__Host-sid")?.value;
  if (!sid) return NextResponse.redirect(new URL("/login", req.url));
  return NextResponse.next(); // 実際の検証はサーバー側で sid を照合
}
export const config = { matcher: ["/member/:path*"] };

Cookie の存在確認だけでは不十分です。Route Handler や Server Component で、必ずセッションストアと照合してください。

CMS(WordPress 等)

「パスワード保護」「会員プラグイン」など、サーバー側で判定される機能を使い、テーマのJavaScriptで表示を切り替えるだけの限定公開は避けます。添付ファイル(PDF等)はメディアURLから直接取得できる場合があるため、別途アクセス制御が必要です。

段階的な導入とガードレール

  1. 保護対象URLの棚卸し(リンク元、ブックマーク、外部委託先の利用状況)。
  2. サーバー認証付きの新URLを用意し、旧URLから302で誘導して利用者影響を確認。
  3. 旧静的ファイルを拒否(404/403)へ切り替え。
  4. localStorage 内の既存トークンは、ログアウト時に removeItem で削除し、サーバー側でも失効させる(ASVS V14.3.1)。

設定後の確認方法

# Cookie なしで保護ページが本文を返さないこと(302/401/403/404 を期待)
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.lg.jp/member/menu.php
# 旧静的URLが拒否されること
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.lg.jp/member/menu.htm

修正後はセキュリティAIチェッカーで再診断し、この項目が○になること、あわせて「認証後静的ファイルの未認証到達性」で未認証のまま本文が返らないことを確認します。

よくある質問(FAQ)

sessionStorageに「ログイン済み」を保存して画面を切り替える方法はなぜ危険なのですか?

sessionStorageの値は、利用者がブラウザの開発者ツールのコンソールで自由に作れるからです。さらに、保護したつもりのページ自体はサーバーが無条件に返しているため、URLを直接開くだけで閲覧できます。実質的に公開状態と変わりません。

アクセストークンをsessionStorageやlocalStorageに保存するのは違反ですか?

直ちに違反ではありません。ASVS 5.0.0 V14.3.3はブラウザストレージに機微データを置かないことを求めつつ、セッショントークンを例外としています。問題は、保存値の有無だけで表示を許可したり、サーバーが送られた値を検証せずに信用したりする使い方です。

キー名や値を難読化すれば安全になりますか?

なりません。照合がブラウザ内で行われている以上、処理を追えば同じ値を再現できます。認可の判定はサーバー側で行う必要があります。

クライアント保存値を認証判定に使っていないかの診断で△になった場合、必ず脆弱性がありますか?

必ずではありません。キー名に auth・token・session・login・user のいずれかを含む getItem/setItem があると△になるため、アクセス解析の user_visit のような無関係な用途でも一致します。保護ページを Cookie なしで直接取得して本文が返らないかを手動で確認し、認証用途でなければ対応は不要です。

クライアント保存値を認証判定に使う兆候が、×(要改善)にならず△(要確認)なのはなぜですか?

HTMLの静的検査だけでは、保存値を認可の根拠にしているのか、トークンをAPIへ送ってサーバーが検証しているのかを区別できないためです。実際に認可が欠落しているかは、関連項目の未認証到達性の確認や手動確認で確定させます。

パスワードの照合をサーバーで行っていれば安全ですか?

照合結果をクライアント保存値として持ち、その後の保護ページの表示がその値の確認だけに依存しているなら、照合後の保護は無いのと同じです。保護ページもサーバー側でセッションを検証してから返す必要があります。

セキュリティAIチェッカーを利用する

この項目を含む127項目を、URLを入れるだけで無料診断します。

参考資料