はじめに
Webアプリケーションを公開する際、意外と見落とされるのが .git ディレクトリの公開です。
Git管理用のファイルは開発者にとっては当たり前の存在ですが、Webサーバー上に残ったまま公開されると、リポジトリの履歴やソースコードを第三者が復元できてしまいます。
この記事の主張を先に一行で言うと、「ディレクトリ一覧が見えなくても .git は復元できる。だから遮断ではなく、そもそも配置しないことがゴールになる」 ということです。CTF(TryHackMe の Room 404)で挙動を再現しつつ、実務でどう混入し、どう塞ぎ、混入を見つけた後に何をすべきかまで通しで扱います。
免責事項
本記事の検証は、TryHackMe が学習用に提供する演習ホストに対してのみ実施しています。自分に権限のないサーバーへのポートスキャンやアクセスは不正アクセス禁止法に抵触します。本記事の内容は、自分が管理・許可された環境の安全性を確認する目的でのみ利用してください。
まず自分の環境を30秒でチェックする
技術的な話に入る前に、手を動かせるコマンドを置いておきます。自分の管理下のサイトに対して実行してください。
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/.git/HEAD
# 200 が返ってきたらアウト。.git が公開されている
# 403 / 404 なら(ひとまず)遮断できている
.git/HEAD はほぼ全リポジトリに存在し、パスも固定です。ここが 200 OK を返すなら、後述のとおりソースコード全体が復元されうる状態だと考えてください。
外形監視やCIのスモークテストにこの1行を組み込んでおくと、再発を継続的に検知できます。
環境調査(CTF)
対象ホストにポートスキャンを実施します。
nmap -sC -sV 10.10.x.x
結果(抜粋):
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH
8080/tcp open http Werkzeug httpd Python
HTTPサービスが8080番で動作しています。ここで Nmap の http-git スクリプトが次を検出しました。
| http-git:
| 10.10.x.x:8080/.git/
| Git repository found!
.git ディレクトリが公開されている状態です。
.git が公開されると何が危険なのか
Gitリポジトリには次のような情報が保存されています。
- ソースコード(現在のツリー全体)
- 過去のコミット履歴
- 開発者コメント
- 削除済みのファイル
- 設定ファイル
- 場合によっては認証情報(
.env、config.php、database.yml、credentials.jsonなど)
重要なのは、現在のWebページから削除されているファイルでも、履歴に残っていれば復元できる という点です。「本番ディレクトリから消したから大丈夫」は通用しません。
「ディレクトリ一覧が見えなくても」復元できる仕組み
ここがこの記事の核です。
多くの記事は「ディレクトリリスティングが有効だったから中身が見えた」で終わります。すると読者は「うちはリスティングを切っているから安全」と誤読しがちです。これは危険な誤解です。
Gitの内部構造は、ファイルパスが決定的に決まります。つまり一覧が取れなくても、名前を知っていれば直接取りに行けます。
.git/HEAD → ref: refs/heads/main (現在のブランチを指す)
.git/refs/heads/main → コミットの SHA-1 ハッシュ
.git/objects/0f/13550b... → zlib 圧縮された commit オブジェクト
└─ commit → tree → blob と再帰的に辿ると、ソース全体を復元できる
.git/index → ワーキングツリー全ファイルのパス一覧が入っている
.git/packed-refs → 参照がパックされている場合のブランチ/タグ一覧
.git/objects/pack/*.idx,*.pack → オブジェクトがパックされている場合の実体
やることはシンプルです。
-
.git/HEADを取る → 現在のブランチが分かる -
.git/refs/heads/<branch>を取る → 最新コミットの SHA が分かる - SHA から
.git/objects/<先頭2桁>/<残り38桁>のパスを組み立てて取得 - 展開して commit → tree → blob と辿り、ファイルを再構成する
-
.git/indexがあれば、全ファイルのパスが分かるので取りこぼしを防げる
これを自動化しているのが git-dumper です。内部でやっているのは上記の「決め打ちで辿る」処理そのものであり、ディレクトリリスティングは一切必要ありません。
# 実務でも演習でも、まずこれで機械的に吸い出せる
git-dumper http://10.10.x.x:8080/.git/ dumped
cd dumped
以降は、吸い出した .git をローカルで解析します。
取得した .git を解析する
現在のブランチを確認します。
git --git-dir=.git rev-parse --abbrev-ref HEAD
# → main
コミット履歴を確認します。
git --git-dir=.git log
commit 0f13550b4cb13e9f30c61d5b342c532d21e45bda
Author: night-shift <dev@byte-lotus.internal>
initial Byte Lotus guest platform
コミットからファイルを直接取り出せます。
git --git-dir=.git show HEAD:README.md
# Byte Lotus — Guest Experience Platform
Internal staging repository for the guest app and concierge personalization
service.
Staging flag:
THM{REDACTED}
公開されているWebページだけでは見えない情報を、Git履歴から取得できました。実務のシナリオに置き換えれば、この README がステージング用の認証情報や内部ホスト名であってもおかしくありません。
フラグやIPアドレスは伏せています。TryHackMe は回答・フラグの公開を推奨しておらず、実務記事としても生の対象情報を載せない姿勢そのものが安全側の作法です。
なぜ本番に .git が混入するのか(実務の経路)
原因を「開発環境をそのまま公開した」で片づけると再発します。実際の混入経路はもっと具体的です。
-
Docker:
COPY . .で丸ごとコピーしているのに、.dockerignoreに.gitを入れていない -
rsync デプロイ:
--exclude='.git'を付け忘れ、履歴ごと転送される -
CI/CD の artifact 化: チェックアウトしたリポジトリを丸ごと成果物にしている(
git archiveを使えば.gitは含まれない) -
静的ホスティング: S3 / Cloudflare Pages / Netlify 等に、
git cloneした結果をそのままアップロードしている -
手動コピー:
cp -r * /var/www/htmlのようなコピーで隠しディレクトリが残る
ポイントは、どれも「Webサーバーの設定ミス」ではなくデプロイパイプラインの問題 だということです。だから次の対策も、遮断だけでは足りません。
対策 — 4つの層で考える
① そもそも配置しない(本質的対策)
成果物に .git を含めないことが最優先です。
# .git を含めない形で成果物を作る
git archive --format=tar HEAD | tar -x -C ./dist
# Docker なら .dockerignore に必ず入れる
echo ".git" >> .dockerignore
.git は成果物ではなく開発用メタデータであり、本番に置くべきものではありません。
② Webサーバー / CDN で多層に遮断する(保険)
オリジンだけ塞いでも、CDN経由やIP直アクセスの経路が残ることがあります。オリジンとエッジの両方で落とします。
Nginx:
location ~ /\.git {
deny all;
return 404;
}
Apache:
<DirectoryMatch "\.git">
Require all denied
</DirectoryMatch>
加えて、WAF / CDN 層でも /.git/* へのアクセスを遮断しておくと、オリジン設定漏れの保険になります。
③ 検知する
デプロイ前にCIで混入をチェックします。
# 成果物ディレクトリに .git が紛れていないか
find ./dist -name ".git" -print -quit | grep -q . && { echo ".git detected"; exit 1; }
公開後は、冒頭の curl チェックを外形監視に組み込みます。あわせてアクセスログを見ると、自動スキャナに舐められている兆候が分かります。
grep -E '/\.(git|svn|env)' /var/log/nginx/access.log
.git/config へのアクセスが記録されている時点で、スキャナの標的になっていると考えてよいです。
④ 見つけてしまった後の対応(最も抜けやすい)
.git を消して終わり、ではありません。 すでに取得された前提で動く必要があります。
- まず遮断・削除する
- 履歴に認証情報が含まれていないかスキャンする
# 履歴全体からシークレットを探す
gitleaks detect --source . --log-opts="--all"
# または
trufflehog git file://. --only-verified
- 検出された認証情報は すべてローテーション する(APIキー、DB認証情報、トークン等)。露出していた期間、誰かがすでに取得した可能性を排除できないためです。
- 内部ホスト名や設計情報が漏れていた場合は、それ自体を機密として扱い直す
「消せば無かったことになる」わけではない、というのがCTFと実務の一番大きな差です。
まとめ
-
.gitは単なる隠しフォルダではなく、リポジトリの履歴とソースコードそのもの - ディレクトリリスティングが無効でも復元できる。 パスが固定なので直接辿れる
- 現在は削除済みのファイルでも、履歴から復元される
- 本質的対策は「遮断」ではなく「本番に配置しない」こと
- 混入経路の多くはWebサーバー設定ではなくデプロイパイプライン側にある
- 露出を見つけたら、削除だけで終わらせず、認証情報のローテーションまで行う
.git は成果物ではなく開発用メタデータです。「URLでアクセスできないから大丈夫」ではなく、公開されているファイルそのものを一つずつ確認する姿勢が、わずかな設定ミスによる情報漏洩を防ぎます。