結論(認証後静的ファイルの未認証到達性)
認証後静的ファイルの未認証到達性とは、ログイン画面を通った後に表示するはずの静的ファイル(.html・.htm・.txt・.csv)が、URLを直接指定するだけでCookieなしでも取得できてしまう状態です。JavaScriptでパスワードを確認して移動させる作りでは、移動先は誰にでも返る普通のファイルです。保護したいデータは公開ディレクトリの外へ移し、サーバー側セッションを確認するプログラム経由でのみ返します。
この記事のポイント
- ログイン画面のJavaScriptがパスワードを確認しても、サーバーへの直接要求には何の影響もありません。
- 移動先のページに置いた「sessionStorageを見て追い返す」スクリプトは、本文がブラウザに届いた後に動くため保護になりません。
- セキュリティAIチェッカーは、パスワード欄のあるページの遷移先や会員・管理系パスの静的ファイルを、Cookieなしで最大2件直接要求します。
- 40文字を超える中身(ログイン画面以外)が2xxで返れば×(要改善)です。証拠欄には件数だけを表示し、内容は表示しません。
- 対策は、公開ディレクトリ外への移動、認証付き配信スクリプト、旧URLの404化の3点です。
| 診断項目名 | 認証後静的ファイルの未認証到達性 |
|---|---|
| 項目ID | auth.static-target-reachability |
| 分類 | 認証・アクセス制御 |
| 重大度・配点の重み | 最重要(critical) 重み 5 |
| 関連する標準・ガイドライン | OWASP ASVS 5.0.0 V8.3.1(認可を信頼できるサービス層で行う)、V8.2.1(機能レベルのアクセス制限)/OWASP WSTG-ATHN-04(Bypassing Authentication Schema)、WSTG-ATHZ-02、WSTG-CONF-04(Review Old Backup and Unreferenced Files)/CWE-425(Direct Request ('Forced Browsing'))、CWE-602(Client-Side Enforcement of Server-Side Security)、CWE-306(Missing Authentication for Critical Function)/IPA「安全なウェブサイトの作り方」アクセス制御や認可制御の欠落 |
認証後静的ファイルの未認証到達性とは
認証後静的ファイルの未認証到達性とは、ログイン画面を通った後に表示するはずの静的ファイル(.html・.htm・.txt・.csv)が、ログインせずに URL を直接指定するだけで取得できてしまうかどうか、という問題です。
「パスワードを入力すると member/top.html に移動する」「JavaScript で合言葉を確認して data.csv を表示する」といった作りでは、移動先のファイル自体は、Web サーバーが誰にでも返す普通の静的ファイルです。URL を直接入力すれば、ログイン画面を通らずに中身を取得できます。
セキュリティAIチェッカーのこの項目は、パスワード欄のあるページから遷移先・送信先になっている静的ファイルや、会員・管理系のパスにある静的ファイルを、Cookie なしで直接要求し、内容が返ってくるかを確認します。
HTML の兆候から「認証後コンテンツを静的ファイルで直接配信していない」が構成を推定するのに対し、この項目は実際にそのファイルへ到達できるかを確かめます。
なぜ静的ファイルはログイン画面で保護されないのか
Web サーバーは静的ファイルをそのまま返す
Web サーバーは、.html や .csv を要求されると、設定で制限していない限りそのまま返します。ログイン画面の JavaScript がパスワードを確認しても、それは利用者のブラウザ内での処理で、サーバーへの直接要求には何の影響もありません。CWE-602 は、サーバー側で行うべきセキュリティ制御をクライアント側に任せる弱点として、この型を説明しています。
典型的な誤実装
<script>
// 誤り: パスワードがソースに見え、遷移先は直接開ける
function check() {
if (document.getElementById('pw').value === 'kaiin2024') {
sessionStorage.setItem('auth', 'ok');
location.href = 'member/top.html';
}
}
</script>
<!-- member/top.html 側の誤ったガード: JS を無効にすれば、または curl なら素通り -->
<script>if (sessionStorage.getItem('auth') !== 'ok') location.href = '../login.html';</script>
2つ目のガードは、HTML の本文がすでにブラウザへ届いた後に動くため、本文は取得済みです。「見せないようにしている」だけで、「渡さないようにしている」わけではありません。
正しい構成
- 保護したいデータは Web 公開ディレクトリの外に置き、認証チェックをするプログラム(PHP 等)が読み出して返す。
- または、Web サーバー自体の認証(Basic 認証、SSO 連携のリバースプロキシ等)でディレクトリごと保護する。
認証後静的ファイルについてよくある誤解
- 「ファイル名を推測しにくくすれば安全」:URL はブラウザ履歴、Referer、アクセスログ、メール転送、検索エンジンのクロールで漏れます。ログイン画面の JS に遷移先が書かれていれば、ソース表示で即座に分かります。
- 「robots.txt で検索除外したから大丈夫」:robots.txt は公開ファイルで、隠したいパスの一覧を攻撃者に教えることになります。
認証後静的ファイルの未認証到達性の診断基準(セキュリティAIチェッカー)
候補の抽出(追加の通信なし)
トップページと、巡回した最大2ページの HTML を材料にします。
- 認証の兆候があるページ を選びます。
type="password"の input 要素を含むページ、またはsessionStorage/localStorageのgetItem/setItemで auth・token・session・login・user を含むキーを扱うページです。「ログイン」「認証」という単語だけでは兆候とみなしません(「認証保育所」「ISMS 認証」など、自治体サイトに頻出する語での誤検知を避けるため)。 - 1 のうち パスワード欄のあるページ について、ページ内の
location = "…"・location.href = "…"・location.assign("…")・location.replace("…")の遷移先と、パスワード欄を含むフォーム(form の開始タグから最初の終了タグまで)の action のうち、同一オリジンで拡張子が .html/.htm/.txt/.csv のものを候補にします。パスワード欄のあるページ自身の URL は除きます。 - 加えて、取得したページの HTML や巡回結果から得た同一オリジンの URL のうち、パスに
member・members・mypage・my-page・private・secure・protected・staff・internal・admin・dashboardで始まるディレクトリ名/ファイル名(直後が/・.・_・-・末尾)を含む .html/.htm/.txt/.csv を候補にします。 - 1 の兆候ページが無い場合、または 2・3 の候補が無い場合は対象外(na)です。候補は重複を除き、2 を優先して最大2件です。Web Storage の兆候だけでパスワード欄の無いページは、1 の兆候にはなりますが、2 の抽出元にはなりません。
確認方法と判定条件
候補へ Cookie・Referer なしで GET を送ります(リダイレクトは追跡しない、タイムアウト5秒)。180KB を超える応答は、先頭部分だけで判定します。
| 判定 | 条件 | 得点への影響 |
|---|---|---|
| × 要改善(fail) | 1件以上が 2xx で、本文が前後の空白を除いて40文字超、かつ本文にパスワード入力欄を含まない(=ログイン画面ではなく中身が返った) | 重み5 × 0 |
| ○ 問題なし(pass) | 応答を取得できた候補がすべて 3xx(ログイン画面への転送)、401、403、404 等、または 2xx でもログイン画面そのもの・ほぼ空の応答 | 重み5 × 1.00 |
| 対象外(na) | 認証の兆候がない、候補が無い、またはタイムアウト等で全候補の応答を取得できない | 計算対象外 |
証拠欄には「n/m 件が Cookie なしで内容を返しました」と件数だけを表示し、取得した内容は表示しません。
自治体サイトでの判定例
| サイトの状態 | 判定 |
|---|---|
| 一般的な広報サイト(パスワード欄なし)。本文に「認証」という語があっても対象にならない | 対象外 |
地域団体向けページで、合言葉の入力後に location.href='kaiin/meibo.html' へ移動し、kaiin/meibo.html が 200 で名簿を返す | × |
職員向け staff/manual.html が 302 で SSO ログインへ転送 | ○ |
判定の限界
公開ページからリンクやスクリプトで参照されていない保護ファイルは発見できません。ログイン後のメニューに並ぶファイルは、手動で一覧化して同じ確認を行ってください。
認証後静的ファイルに未認証で到達できるとどんなリスクがあるか
- 個人情報・内部資料の流出:会員名簿、利用者一覧 CSV、議事録案、マニュアル等を URL の直接指定で取得されます。
- パスワードの露出:クライアント側でパスワードを照合する実装では、HTML/JS のソースにパスワード(またはそのハッシュ)が含まれていることが多く、総当たりも容易です。
- 検索エンジンへの掲載:どこかにリンクされた保護ファイルがクロールされ、検索結果に表示されることがあります。
- 気づきにくさ:アクセスログ上は通常の GET と区別がつかず、流出に気づくのが遅れます。
- CWE-425(強制ブラウジング:認証画面を経由せず URL を直接要求する)として古くから知られる型で、OWASP WSTG-ATHN-04 の目的「認証を必要とするすべてのサービスに認証が適用されていること」の確認対象そのものです。
流出シナリオの例(地域団体の会員名簿)
- 地域団体向けのページで、合言葉を入れると JavaScript が
kaiin/meibo.htmlへ移動する作りになっている。 - 第三者がページのソースを表示し、合言葉と移動先の URL を見つける。合言葉を知らなくても、移動先の URL さえ分かれば十分である。
- 第三者が
kaiin/meibo.htmlを直接開くと、Web サーバーは名簿をそのまま返す。 - アクセスログには通常の閲覧と同じ GET が残るだけで、担当者は流出に気づかない。
名簿を公開ディレクトリの外へ移し、セッションを確認する配信スクリプト経由にしていれば、手順3の要求は 403 や 404 になります。
認証後静的ファイルを保護する方法(PHP・Apache・nginx・IIS・Next.js)
基本方針
- 保護対象のファイルを Web 公開ディレクトリ(DocumentRoot)の外へ移動する。
- サーバー側セッションを検証するプログラム経由でのみ返す。
- 旧 URL は 404(または 410)にし、内容を返さない。
PHP(配信スクリプト)
<?php
// download.php?f=meibo.csv
session_start();
if (empty($_SESSION['user_id'])) { http_response_code(403); exit; }
$name = $_GET['f'] ?? '';
if (!preg_match('/\A[a-z0-9_-]{1,64}\.(?:csv|txt|html)\z/', $name)) { http_response_code(400); exit; }
$base = realpath(__DIR__ . '/../private_data'); // 公開ディレクトリ外
$path = realpath($base . '/' . $name);
if ($path === false || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) { http_response_code(404); exit; }
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
header('Content-Disposition: attachment; filename="' . $name . '"');
readfile($path);
ファイル名は、ホワイトリストの正規表現と realpath の両方で検証し、ディレクトリ・トラバーサルを防ぎます。
Apache(暫定措置)
移行が終わるまで、旧ディレクトリを全拒否します。
# /member/.htaccess
Require all denied
または、Web サーバーの認証で保護します(AuthType Basic と Require valid-user、パスワードファイルは公開ディレクトリの外に置く)。
nginx
location /member/ { internal; } # 外部からの直接要求は 404
# アプリが認証後に X-Accel-Redirect: /member/xxx.csv を返したときだけ配信
IIS
web.config の requestFiltering → hiddenSegments に <add segment="member" /> を追加して直接要求を 404 にし、ASP.NET 等の認証付きハンドラーから配信します。
Next.js
保護ファイルを public/ に置かないでください。public/ 配下は認証なしで配信されます。サーバー側の Route Handler でセッションを確認してから読み出して返します。
設定後の確認方法
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" https://www.city.example.lg.jp/kaiin/meibo.html
# 期待: 403 / 404 / 302(ログイン画面へ)
ログイン画面のソースに残っている遷移先の URL や、ブラウザ履歴にある URL を一覧化し、すべて確認してください。
サイトを壊さないための注意点(ガードレール)
- 移動する前にファイルのバックアップを取り、外部から参照されているリンク(他課のページ、PDF 内のリンク)を洗い出します。
- 既に流出した可能性がある場合は、アクセスログで該当 URL への外部からの 200 応答を確認し、個人情報が含まれていれば組織の手順に従って報告します。
- クライアント側に書かれていたパスワードは、漏えいしたものとして変更してください。
よくある質問(FAQ)
JavaScriptでパスワードを確認するログインは安全ですか?
安全ではありません。照合は利用者のブラウザ内の処理なので、移動先のファイルのURLを直接入力すれば誰でも取得できます。また、パスワード(またはそのハッシュ)がHTMLやJSのソースに含まれるため、総当たりも容易です。
認証後静的ファイルにアクセスしてログイン画面への転送や403が返る場合、どう判定されますか?
○ 問題なしです。候補へCookie・RefererなしでGETを送り、応答を取得できた候補がすべて3xx(ログイン画面への転送)、401、403、404等、または2xxでもログイン画面そのもの・ほぼ空の応答であれば合格とします。1件以上が2xxで、本文が前後の空白を除いて40文字超、かつパスワード入力欄を含まない場合は×です。
認証後静的ファイルの未認証到達性の診断で、取得したファイルの内容は表示されますか?
表示されません。証拠欄には「n/m 件が Cookie なしで内容を返しました」と件数だけを表示し、取得した内容は表示しません。また、180KBを超える応答は先頭部分だけで判定します。
Next.jsのpublicフォルダに会員用ファイルを置いてもよいですか?
置かないでください。public/ 配下は認証なしで配信されます。サーバー側の Route Handler でセッションを確認してから読み出して返します。
パスワード欄の無いサイトでも診断されますか?
認証の兆候(パスワード欄、または認証・トークン系のキーを扱う sessionStorage/localStorage)が無いページしかなければ「対象外」です。本文に「認証」という語があるだけでは対象になりません。
すでに流出していた可能性がある場合はどうすればよいですか?
アクセスログで該当URLへの外部からの200応答を確認し、個人情報が含まれていれば組織の手順に従って報告します。クライアント側に書かれていたパスワードは漏えいしたものとして変更してください。
この項目を含む127項目を、URLを入れるだけで無料診断します。
参考資料
- OWASP ASVS 5.0.0 V8 Authorization
- OWASP WSTG-ATHN-04 Testing for Bypassing Authentication Schema
- OWASP WSTG-ATHZ-02 Testing for Bypassing Authorization Schema
- CWE-425 Direct Request ('Forced Browsing')
- CWE-602 Client-Side Enforcement of Server-Side Security
- nginx「internal」ディレクティブ(ngx_http_core_module)
- IPA「安全なウェブサイトの作り方」