対策・学習23分で読める

公開ディレクトリに秘密ファイルを置き忘れていないか:webrootの棚卸し

トークンや認証情報のファイルをWeb公開ディレクトリ(webroot)に置き忘れると、URLを直接叩けば誰でも取得できます。共通テンプレート由来で全サイトに広がる問題と、棚卸しして塞ぐ手順を解説します。

本文を読む
公開日
更新日
目次

対象:レンタル/共用サーバーやVPSで、複数のサイトを公開ディレクトリ(public/・public_html/・htdocs/ 等)にデプロイして運用している人。ここでは攻撃手順は扱わず、自分のサーバーを棚卸しして塞ぐことだけを扱います。.envそのものの守り方は 共用サーバーでの.env保護 も参照。

当サイトの視点:『置き場所』はアクセス制御そのもの

秘密が漏れる原因は、難しい攻撃よりも置き場所の事故が多いです。webrootは『誰でもURLで取りに来られる場所』。そこに秘密を置いた時点で、どんなに強いパスワードのサーバーでも意味がありません。当サイトは秘密をwebrootにもgitにも置かないことを基本にしています(→ 秘密の安全な保管)。考え方はシンプルで、公開ディレクトリには誰に見られても困らない物だけを置きます。

public/ = URLで誰でも取得可

置いてよい

画像・CSS・JS など公開アセット

置くな

.env・token.json・.sql・.git・.bak

アプリのルートより上 = 配信されない

ここに置く

秘密・トークン・認証情報・バックアップ。権限600(所有者だけ読める)。URLからは到達できない。

公開ディレクトリのファイルはURLで誰でも取得できる。秘密はその外へ(配信されない場所・権限600)。

なぜ「公開ディレクトリの置き忘れ」が起きるのか

実際にありがちなのは、便利のために置いたファイルや、ひな形に最初から入っていたファイルが、そのまま公開され続けるパターンです。

URLだけ
webrootのファイルは誰でも取得可
年単位
気づかず放置されがち
全サイト
テンプレ由来は全サイトに広がる
即失効
漏れたら『漏れた前提』で対応

たとえば、外部サービス連携のために作ったアクセストークン入りのJSONが、public/ の下に置かれたまま長期間放置される。しかもそれが共通テンプレート由来だと、同じファイルが複数サイトに同じ場所でコピーされ、全サイトが同じ問題を抱えます。1サイトを直しても、他のサイトに残っていれば意味がありません。同様に危ないのは、.env・データベースのバックアップ(.sql)・.git ディレクトリ・エディタのバックアップ(*.bak)・ショートカット等の置き忘れです(関連:パストラバーサル・.envが丸ごと公開された話)。

置き忘れやすいファイルと、それぞれの危険

「秘密のファイル」と聞くと .env を思い浮かべがちですが、実際に残りやすいのはもっと幅が広いです。OWASP(Webセキュリティの非営利団体)のテストガイドも、古い版のファイルやバックアップ、どこからもリンクされていないファイルを点検項目に挙げています。

種類よくある名前の例何が危ないか置くべき場所
設定ファイル.env、config.php.bak、settings.jsonデータベースのパスワードやAPIキーがそのまま書かれているアプリのルートより上(配信されない場所)
トークン・認証情報token.json、credentials.json、*.pem、*.key外部サービスや別のサーバーに、そのままログインできるwebrootの外+権限600
手作業・エディタのバックアップlogin.php.old、index.php~、.index.php.swp、*.orig拡張子が変わるとプログラムとして実行されず、ソースコードがそのまま表示されることがある本番では作らない。作業は手元で行う
データベースのダンプbackup.sql、dump.sql.gz利用者の個人情報やパスワードのハッシュが丸ごと入っているバックアップ専用の場所(サーバー外が望ましい)
サイトを固めたファイルsite.zip、www.tar.gz中に .env やソースコード一式が入っている展開したらすぐ削除
バージョン管理のデータ.git/過去のコミットに入っていた秘密まで復元されるおそれがある公開ディレクトリの外
ビルドで埋め込まれた値assets/index-xxxx.js の中VITE_・NEXT_PUBLIC_ で始まる変数はブラウザ向けのJavaScriptに入る秘密はサーバー側の変数だけにする

表の最後の行は見落とされがちです。ファイルとしては置いていなくても、ビルドの結果として秘密が公開ディレクトリに出ていくことがあります。Viteの公式ドキュメントは、VITE_ で始まる変数にAPIキーなどの機微な情報を入れてはいけないと明記しています。

自分のサーバーを棚卸しする

まず「自分の公開ディレクトリに、秘密らしきファイルが無いか」を機械的に洗い出します。これは自分の資産の点検であって、他者のサイトを覗く行為ではありません。

1

公開ディレクトリ配下の秘密らしきファイルを探索

自分のホーム配下で、public/ 等に紛れた秘密候補(トークン/認証情報のJSON・.env・鍵・ショートカット)を洗い出す。
2

同じ物が他にもある前提で『全台・全サイト』へ広げる

同じひな形から作ったサイトは同じ場所に同じ物がある前提で、全ホスト・全サイトに同じ点検をかける。1件見つかれば、他にもある可能性が高い。
3

本当に公開が必要かを1つずつ判断

出てきたファイルが『公開してよい物』か『秘密』かを仕分ける。秘密なら次節の手順で除去・失効・移動。
# 自分のサーバーの公開ディレクトリ配下を棚卸し(自分の資産の点検)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
      -o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# .git ディレクトリの公開も危険
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/null

自分の環境で確認する方法

上の find は入口です。次の5つを順に確認すると、ファイル名の探索だけでは見つからない置き忘れも拾えます。どれも自分のサーバー・自分のドメインに対してだけ行ってください。.env に絞った確認(中身の先頭を見る方法や .htaccess が効いているかの確かめ方)は 共用サーバーでの.env保護 にまとめています。

1

バックアップや圧縮ファイルも探す

公開ディレクトリの名前は環境によって違うので、public_html・htdocs・www も含めて探します。たとえば find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null のように実行し、出てきたファイルが本当に公開する物かを1つずつ確かめます。

2

ディレクトリの一覧が表示されないか確かめる

index.html などを置いていないディレクトリ(例:/uploads/)をブラウザで開きます。ファイル名の一覧が表示されたら、Webサーバーの「ディレクトリ一覧」機能が有効です。一覧が出ると、名前を知らなくても置き忘れに気づかれます。403か404が返るのが正常です。

3

ビルド後のJavaScriptに秘密が入っていないか探す

フロントエンドをビルドしているなら、配信する出力フォルダ(dist/ や .next/static/ など)の中を検索します。grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/ のように、秘密鍵の見出し行や、使っているサービスのキーの書き出し部分で探します。見つかったら、その変数がブラウザ向けの名前(VITE_・NEXT_PUBLIC_)になっていないかを確認します。

4

webrootの外の秘密が、他の利用者から読めないか確かめる

秘密をwebrootの外に移しても、権限が緩いと同じサーバーの別アカウントから読めることがあります。find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null で、所有者以外にも読める秘密を洗い出し、chmod 600 で所有者だけにします。何も出なければ正常です。

5

検索エンジンに載っていないか確かめる

Googleで site:自分のドメイン と検索し、見覚えのないファイル(.sql、.zip、.txt など)が結果に出ていないかを見ます。Search Consoleを使っているなら、「ページ」のレポートで登録済みのURLを一覧できます。検索結果に残っていた場合の対処は、下の「よくある間違い」で説明します。

見つけたときの対応

見つけたら、除去・失効・移動の3点をセットで行います。どれか欠けると対処が不完全になります。

1

webrootから除去し、URLで取れない状態に

該当ファイルを公開ディレクトリの外へ移すか削除し、URLを叩いても404になることを確認する。
2

漏れた可能性のある鍵・トークンは失効/再発行

『たぶん大丈夫』で残さない。露出していた以上、漏れた前提で該当トークン・鍵をリフレッシュまたは失効させる。
3

秘密はwebrootの外+権限600へ

今後は秘密を公開ディレクトリの外(アプリのルートより上など)に置き、権限を600(所有者だけが読める)にする。設定で機微な拡張子の配信を拒否できるなら併用する。

サーバー別:配信を拒否する設定(保険として)

本命は「置かないこと」です。そのうえで、うっかり置いてしまったときに配信されないよう、Webサーバー側にも拒否の設定を入れておきます。使えるものは環境によって違います。

環境拒否の書き方ディレクトリ一覧注意点
Apache(VPS)設定ファイルの <Directory> か .htaccess に <FilesMatch> + Require all deniedOptions -Indexes で止めるApache 2.4 は AllowOverride の既定が None で、許可しないと .htaccess が読まれない
Nginx(VPS)location で deny all;autoindex は既定で off.htaccess は読まない。正規表現の location は書いた順に照合され、最初に一致したものが使われる
共用レンタルサーバー多くは .htaccess(Apache互換)で書ける管理画面か .htaccess で止める効いているかは、秘密を含まないダミーのファイルで確かめる
静的ホスティング・CDN出力フォルダの中身がそのまま公開されるサービスによる拒否設定より、出力フォルダに秘密を入れない運用が前提

Apacheの例(.htaccess が有効な場合):

Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
    Require all denied
</FilesMatch>

Nginxの例(server ブロックの中):

# 証明書の自動更新(Let's Encrypt)で使う場所は先に許可する
location ^~ /.well-known/acme-challenge/ { }
 
# ドットで始まるファイル・ディレクトリ(.env, .git など)を拒否
location ~ /\. { deny all; }
 
# バックアップや圧縮ファイルを拒否
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }

Nginxでは、^~ を付けた場所に一致すると、その後の正規表現の照合が行われません。そのため、証明書の更新に使う /.well-known/acme-challenge/ は、ドットで始まるパスを拒否するルールの影響を受けずに配信されます。設定を変えたら nginx -t で書式を確かめてから読み込み直し、ダミーのファイルで403が返ることを確認します。ZIPなどを利用者向けに配布しているサイトでは、その拡張子を拒否の対象から外してください。

やりがちな配置(危険)

  • 連携用トークンのJSONを public/ に置く
  • ひな形に入っていた秘密ファイルをそのまま公開
  • バックアップ(.sql/.bak)や .git を公開下に放置
  • 1サイトだけ直して『対応完了』

正しい配置

  • 公開ディレクトリには公開してよい物だけ
  • 秘密はwebrootの外+権限600
  • 機微な拡張子は配信を拒否(設定で deny)
  • 1件見つけたら全台・全サイトを棚卸し

『公開して良い物だけ置く』を運用ルールに

個別の対処より、ルール化が効きます。公開ディレクトリには公開してよい物だけを置くと決め、秘密・バックアップ・バージョン管理データ(.git)・設定ファイルは原則そこに置かない。ひな形(テンプレート)を更新するときは、ひな形側から秘密を抜くようにします。そうしないと、次に量産するサイトにも同じ秘密ファイルがコピーされ続けます。

よくある間違いと直し方

  • 拒否の設定だけで安心して、ファイルを残す:サーバーの移行や設定の書き換えで拒否が外れると、その日から公開されます。Nginxでは .htaccess が読まれません。設定は保険と考え、ファイル自体を公開ディレクトリの外へ出します。
  • 本番サーバーの上で直接ファイルを編集する:エディタが ~ や .swp の付いたバックアップを作り、そのまま残ります。OWASPのテストガイドも、Webサーバー上でファイルをその場で編集しないよう勧めています。編集は手元で行い、デプロイで反映します。
  • プロジェクトのフォルダを丸ごと公開ディレクトリへコピーする:.env・.git・テスト用のデータまで一緒に出ていきます。配信するのはビルドの出力フォルダだけにし、コピーの対象を明示します。
  • ドットで始まるパスを一律に拒否し、証明書の更新が失敗する:Let's Encryptの確認用ファイルは /.well-known/acme-challenge/ に置かれます。上のNginxの例のように、この場所だけ先に許可します。
  • 秘密を VITE_ や NEXT_PUBLIC_ の変数に入れる:ビルドしたJavaScriptに埋め込まれ、誰でも読めます。サーバー側の処理でだけ使い、過去にこの形で配布していたキーは失効させます。
  • ファイルを消して終わりにする:検索エンジンに登録されていると、結果に残ることがあります。URLが404か410を返すことを確かめ、Google Search Consoleの「削除」ツールで一時的に非表示にします(約6か月)。そのあいだに再クロールで消えるのを待ちます。

実例:公開した報告書に、隠れた個人データが残っていた

米ワシントンDCでメディケイド(低所得者向けの公的医療保険)を運営する医療保険財政局(DHCF)は、同局のウェブサイトに2023年から2026年7月まで掲載していた統計報告書2件に、集計の元になった個人データが「隠しフィールド」として含まれていたと公表しました。画面には集計の数字しか表示されない作りでしたが、ファイルの中には受給者のメディケイド番号や生年月日などが残っていました。米保健福祉省(HHS)への届出では、対象は399,086人とされています。DHCFは2026年7月21日に把握し、報告書を削除しました。

これは秘密のファイルを置き忘れた例ではありませんが、根は同じです。

  • 公開ディレクトリに置いたファイルは、画面に表示されている部分だけでなく、ファイルの中身ごと誰でも取得できる
  • 載せた側が「見えていない」と思っている情報ほど、点検から漏れやすい
  • 何年も掲載されたままになり、気づいたときには長期間公開されていた

公開する前に「このファイルの中に何が入っているか」を確かめる手順を持つことが、置き忘れと隠れたデータの両方を防ぎます。詳しい経緯は 米ワシントンDCのメディケイド情報露出 にまとめています。

当サイトはどうしているか

当サイトは、秘密(鍵・トークン・接続情報)を公開ディレクトリにもコードのリポジトリにも置かないことを基本にしています。デプロイ成果物に含めるのはビルド済みの公開アセットだけで、秘密は実行環境の環境変数として、配信されない場所に保持します。理由は、この記事の事故クラスが「1つの置き忘れが、URLを指定するだけで全部漏れる」典型だからです。置き場所そのものをアクセス制御として設計し、棚卸しは 脆弱性対応の棚卸し と同じタイミングで一緒に回しています。

次に読む

出典

  • IPA「安全なウェブサイトの運用管理に向けての20ヶ条」(公開すべきでないファイル、検索エンジンのキャッシュ): ipa.go.jp
  • OWASP「Web Security Testing Guide:Review Old Backup and Unreferenced Files for Sensitive Information」(.old・~・.swp などのバックアップ、本番で直接編集しない): wstg.owasp.org
  • Apache HTTP Server 2.4「Upgrading to 2.4 from 2.2」(AllowOverride の既定が None)/「mod_autoindex」(Options +Indexes で一覧を生成): httpd.apache.org、httpd.apache.org
  • nginx「ngx_http_core_module:location」(照合の順序と ^~)/「ngx_http_autoindex_module」(autoindex の既定は off): nginx.org、nginx.org
  • Let's Encrypt「Challenge Types」(HTTP-01 は /.well-known/acme-challenge/ を使う): letsencrypt.org
  • Vite「Env Variables and Modes」(VITE_ の変数はクライアントのコードに公開される): vite.dev
  • Next.js「Environment Variables」(NEXT_PUBLIC_ の変数はブラウザ向けのJavaScriptに埋め込まれる): nextjs.org
  • Google Search Console ヘルプ「削除ツール」(一時的な非表示は約6か月): support.google.com
  • District of Columbia DHCF「Notice of Data Incident」: dhcf.dc.gov/HIPAA Journal(HHSへの届出人数): hipaajournal.com

よくある質問

Q公開ディレクトリにファイルを置くと何が危険ですか?
A

Web公開ディレクトリ(webroot=public/ や public_html/ など)に置いたファイルは、URLを直接叩けば誰でも取得できます。トークン・認証情報のJSON・.env・バックアップ・鍵ファイルなどを置き忘れると、人が気づかないうちに取得され、即・実害につながります。公開ディレクトリには『公開してよい物だけ』を置くのが原則です。

Q1サイトで見つかったら、他も点検すべきですか?
A

すべきです。共通テンプレートやひな形からサイトを量産していると、同じ置き忘れが全サイトにコピーされていることが多いからです。1か所で見つけたら、同じ由来の全サイト・全ホストを一括点検してください。

Q見つけたらどう対応しますか?
A

①該当ファイルをwebrootから除去し、URLで取得できない(404)状態にする②漏れた可能性のあるトークン・鍵はリフレッシュ/失効させる(漏れた前提で動く)③秘密は今後webrootの外に置き、権限を600(所有者のみ読める)にする。この3点をセットで行います。

Q公開ディレクトリに置いてはいけないファイルには何がありますか?
A

代表的なのは、.env などの設定ファイル、トークンや認証情報のJSON、データベースのダンプ(.sql)、サイトを丸ごと固めたZIPやtar.gz、.git ディレクトリ、エディタや手作業で作ったバックアップ(.bak・.old・.orig・.swp・末尾が ~ のファイル)です。OWASPのテストガイドは、login.php を login.php.old のように拡張子を変えて残すと、プログラムとして実行されずに中身のソースコードがそのまま表示されることがある、と説明しています。

Qファイルを消したのに、検索結果に残っています。どうすればいいですか?
A

まずファイルを削除してURLが404か410を返す状態にし、そのうえでGoogle Search Consoleの「削除」ツールで一時的に検索結果から外します。Googleのヘルプによると、この一時的な非表示は約6か月で、ファイル自体を消すなどの恒久的な対応と組み合わせる前提です。IPAも、検索エンジンのキャッシュに残っている場合は削除を依頼するよう案内しています。中に含まれていた鍵やトークンは、検索結果から消えたかどうかに関係なく失効させてください。

Qフロントエンドの環境変数(VITE_ や NEXT_PUBLIC_)に秘密を入れても大丈夫ですか?
A

大丈夫ではありません。Viteは VITE_ で始まる変数を、Next.jsは NEXT_PUBLIC_ で始まる変数を、ビルド時にブラウザへ配るJavaScriptに埋め込みます。つまり公開ディレクトリにファイルを置くのと同じで、誰でも読めます。APIキーなどの秘密は、サーバー側の処理だけで使う変数にしてください。