0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

公開されるのは本文だけではない。grep検査を全部通したのにコミット履歴から個人情報が漏れていた話

0
Last updated at Posted at 2026-09-05

個人情報を公開物に出さない、という運用ルールを持っている。本名・住所・電話番号・個人のメールアドレスは、Webサイトにも商品説明にもREADMEにもHTMLコメントにも書かない、というやつだ。公開前には本文を grep で全数検査していて、その結果はずっと0件だった。

2026-09-06、別件のついでに公開リポジトリのコミット著者欄を機械で数えてみた。同日の実測でこうなった。

  • 公開リポジトリ 12本 / 走査したコミット 449件
  • そのうち 11本 に、個人を特定できる識別子が author または committer として残っていた

本文の検査は正しく動いていた。壊れていたのは検査の中身ではなく、検査の対象範囲のほうだった。公開されるのは本文だけではない。

公開リポジトリ12本449コミットのうち11本に個人識別子が残っていた一方、本文のgrepは0件だった要約

目次

1. 何が漏れていたのか

1-1. コミットの著者欄は本文ではない

Gitはコミットごとに、著者(author)と確定者(committer)の名前とメールアドレスを焼き込む。ここに入るのは git configuser.name / user.email の値で、リポジトリ内のどのファイルにも書かれていない。だからファイル本文をいくら grep しても出てこない

そして公開リポジトリなら、これは誰でも読める。

# 公開されている実体を、GitHub の API で読む
gh api "repos/<owner>/<repo>/commits?per_page=100" --paginate \
  --jq '.[] | "\(.sha[0:7]) \(.commit.author.email) | \(.commit.committer.email)"'

レスポンスに commit.author.email が含まれることは公式リファレンスに明記されているし、gh apiページングにも対応している。要するに隠れているわけではなく、単に誰も見ていないだけの場所だった。

自分の場合、ここに気づいたきっかけも偶然だった。別の作業でコミット履歴を眺めていて、見慣れない文字列が author に入っているのが目に留まった。狙って探したわけではない。だから怖い。

1-2. メールを設定していないほうが危ないことがある

見つかった識別子は2種類あった。

種類 どこから来るか 見つけにくさ
個人のメールアドレス git config user.email にそのまま入れた値 警戒されやすい。それでも残る
ユーザー名@ホスト名.local user.email が未設定のときにGitが合成する値 ほぼ誰も見ていない

危ないのは後者だ。git-commit のドキュメントには、著者情報の決定順がこう書かれている。環境変数 → user.name / user.emailEMAIL 環境変数 → それも無ければ「システムのユーザー名と、送信メール用のホスト名」を繋いだもの。つまり設定していないと、Gitが勝手にアドレスを組み立てる

そしてmacOSの既定のホスト名は、初期設定で入力した名前から作られる。「〇〇のMacBook Air」という共有名がそのまま 〇〇noMacBook-Air.local になるので、アカウント名がホスト名に入る。本名でアカウントを作っている人は少なくないはずだ。

メールアドレスを一度も書いていないのに、マシンの名前経由で個人が特定できる文字列が公開コミットに焼き込まれる。「自分はメールを設定していないから大丈夫」は、むしろ逆になりうる。

2. 12リポジトリを数えた結果

2-1. 内訳

リポジトリ別の走査コミット数と検出された識別子の種類数の内訳

数えた結果を、リポジトリ名を伏せて並べるとこうなる(2026-09-06 実測)。

リポジトリ 走査コミット数 検出された識別子
A 173 1
B 87 2
C 31 3
D 30 2
E〜K(7本) 5〜26 1〜2
L 11 0

きれいだったのは12本中1本だけ。しかもその1本は、たまたま最初から noreply を設定してあっただけで、意図して守れていたわけではない。運で守れていたものは、守れているとは言わない。

コミット数が多いリポジトリほど識別子の種類が増えるわけでもない、というのは少し意外だった。173コミットのリポジトリは1種類で、31コミットのリポジトリが3種類。環境を作り直した回数のほうが効いているようだった。

2-2. 何を「許可」とみなすか

検査を書くとき最初に決めたのは、許可リストのほうだった。ここが曖昧だと、件数が正しく出ない。

  • *.users.noreply.github.com … GitHubが配る転送用アドレス。個人を特定しない
  • github-actions[bot] などのボット … CIが作るコミット
  • それ以外は全部「要確認」に落とす

「たぶん大丈夫なドメイン」を許可に足したくなるが、そこを緩めると検査の意味がなくなる。会社のドメインだから安全、という判断も本当は成り立たない。そのアドレスが公開されてよいかどうかは、ドメインではなく本人しか決められない。 だから許可はボットと noreply の2種類に固定して、あとは全部人間が見る、という設計にした。

2-3. 数える前に予想を書いておく

これは検査の話というより自分への手当てだが、走らせる前に「たぶん2〜3本くらい汚れているだろう」と書き残しておいた。実際は11本だった。予想を先に書いておくと、結果が想定と桁で違ったときに、自分の見積もりのほうを疑える。今回は「たまに漏れる」ではなく「既定では漏れる」が正しい理解だった、と分かった。

3. なぜ公開前の検査を通ったのか

3-1. 検査の対象が成果物の本文に限られていた

これが根本原因だった。「公開されるもの」を本文だと思い込んでいた。実際に外から読めるものを並べると、本文はその一部でしかない。

本文のgrepで見つかるものと、見つからない公開物(コミット著者欄・ブランチ名・メタデータなど)の対比

公開される場所 中身 本文の grep で見つかるか
HTML / JS / CSS の本文 書いたテキスト 見つかる
コミットの著者欄 git config の値・ホスト名 見つからない
ブランチ名・タグ名 作業者が付けた名前 見つからない
ファイル名・ディレクトリ名 日本語ファイル名など 見つからない
OGP画像・PDFのメタデータ 作成者・アプリ名 見つからない
Issue / Pull Request / リリースノート やり取りの本文 見つからない
レスポンスヘッダ サーバー構成の手がかり 見つからない

検査を作るときは、「これで公開物を全部カバーしているか」を1問自答する。自分はこれをやっていなかった。

3-2. git log は今いるブランチしか見ない

ここが今回いちばん効いた罠だった。作業に入る前、手順どおりに確認はしていた。

git log --format='%an <%ae>' | sort -u
# → noreply しか出てこない。「きれい」

この出力は嘘ではない。ただし見ていたのは、ローカルにあった別のブランチだった。

経緯はこうだ。前に作業した担当が履歴の汚れに気づき、著者情報を書き換えたブランチをローカルに作っていた。ところがそのリポジトリは force-push が禁止されていて、書き換えた履歴を push できなかった。結果として、直したブランチだけが手元に残り、公開側は汚れたままという状態になっていた。

ローカルのclean-historyブランチと公開されているorigin_mainで著者欄が違っていた図解

作業ブランチがそちらのまま git log を叩けば、きれいな履歴しか見えない。公開されている origin/main の著者欄には、個人メールとホスト名がそのまま残っている。

「直したブランチが手元にある」は「直っている」ではない。 push されていない修正は、存在しないのと同じだ。

正しい確認はこうなる。

# これだけでは足りない(今いるブランチしか見ない)
git log --format='%an <%ae>' | sort -u

# ローカルの全参照を見る
git log --all --format='%an <%ae> | %cn <%ce>' | sort -u

# 公開されている実体を見る(これが本命)
gh api "repos/<owner>/<repo>/commits?per_page=100" --paginate \
  --jq '.[].commit.author.email' | sort -u

--all が「すべての参照から辿れるコミット」を対象にすることはgit-log のドキュメントのとおり。ただし --all でもローカルに無いものは見えないので、最終的な判定は必ずリモートの実体で行う。

4. 設定のタイミングという罠

4-1. 設定は最初のコミットより前でないと効かない

もう一つ、当たり前だが踏みやすい罠がある。

リポジトリを作った直後に git config user.email を設定しても、その前に作って push した最初の1コミットには一切効かない。「新しいリポジトリでは最初に設定する」と決めていても、git initgit addgit commit → 設定、という順番だと手遅れになる。

設定は最初のコミットより前。 できれば --global で最初から入れておく。

4-2. リポジトリごとに使い分けたいときは条件付きインクルード

仕事とプライベートでアドレスを分けたい、という理由で「リポジトリごとに手で設定する」運用にしていると、必ずどこかで忘れる。忘れた1回が公開コミットになる。

Gitには条件付きインクルードがあって、.git ディレクトリの場所で設定ファイルを切り替えられる。

# ~/.gitconfig
[user]
    name  = username
    email = 12345678+username@users.noreply.github.com

[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work

gitdir: に続く値はグロブパターンとして扱われ、.git の位置が一致したときだけ読み込まれる。既定を安全側(noreply)にしておいて、例外のディレクトリだけ上書きする、という向きで組むのが要点になる。逆向きにすると、設定を忘れたときに危ないほうへ倒れる。

5. 全数検査のスクリプト

対象がわかってしまえば、検査自体は短い。アカウント配下の公開リポジトリを全部舐めて、許可した識別子以外が出たら件数を返す。

#!/bin/bash
# 公開リポジトリの「コミット著者欄」に個人識別子が出ていないかを全数検査する
set -uo pipefail
ALLOW='users\.noreply\.github\.com|github-actions|noreply@'

gh repo list --limit 200 --json nameWithOwner,visibility \
  --jq '.[] | select(.visibility=="PUBLIC") | .nameWithOwner' |
while read -r R; do
  EMAILS=$(gh api "repos/$R/commits?per_page=100" --paginate \
             --jq '.[] | "\(.commit.author.email)\n\(.commit.committer.email)"' 2>/dev/null | sort -u)
  N=$(printf '%s\n' "$EMAILS" | grep -Evc "$ALLOW")
  T=$(gh api "repos/$R/commits?per_page=100" --paginate --jq '.[].sha' 2>/dev/null | wc -l | tr -d ' ')
  printf '%-40s commits=%-5s leaked_identities=%s\n' "$R" "$T" "$N"
done

全数検査スクリプトの出力例と、許可する識別子2種類

gh repo list は自分のアカウントの一覧を返すので、Organization を持っているなら --owner を足す。走査は1リポジトリあたり数リクエストで済むから、12本なら数秒で終わる。

抜けやすいのは3か所ある。Organization 配下のリポジトリ、アーカイブ済みのリポジトリ、そして自分が作ったフォークだ。アーカイブしたリポジトリは読み取り専用になるだけで、コミット履歴は今も公開されている。フォークは元の履歴をそのまま引き継ぐので、自分が1コミットも積んでいなくても、自分の名前空間の下に他人の著者情報が並ぶ。逆に、自分の汚れた履歴を誰かがフォークしていれば、その複製はこちらから消せない。数えるときは一覧の条件を先に確認しておいたほうがいい。

5-1. 出荷ゲートに組み込む

単発で流して終わりにすると、次に増えたリポジトリで同じことが起きる。自分は公開前チェックを1本のスクリプトに束ねていて、そこに項目として足した。

hr "6. gitのコミット著者(publicリポジトリ)"
if [ -n "$REPO" ]; then
  em=$(git -C "$REPO" log --all --format='%ae%n%ce' 2>/dev/null | sort -u \
       | grep -Ev "users.noreply.github.com|github-actions" || true)
  [ -z "$em" ] && ok "個人メール・ホスト名 0件" \
                || { echo "$em" | sed 's/^/     /'; fail "コミット著者に個人識別子=pushしない"; }
else
  printf '   - 未指定(publicリポジトリなら --repo を渡す)\n'
fi

ポイントは2つある。--all を必ず付けること。そしてリポジトリを渡さなかったときに「合格」と出さないこと。未指定は未実施であって合格ではない。ここを緑にしてしまうと、検査を通した気持ちだけが残る。

5-2. 検査そのものを両方向で試す

作った検査は、壊れた入力で鳴ることと、正常な入力で黙ることの両方を確認しておく。特に後者を飛ばすと、常に何かを出す検査になり、いずれ誰も読まなくなる。自分は出荷ゲートを1本にまとめた直後に、そこへ「常に偽のNGを出す検査」を1本混ぜてしまった。その失敗と、誤検出を減らすための設計原則は別の記事にまとめてある。

この記事の検査についても同じで、著者欄がきれいなリポジトリに掛けて leaked_identities=0 が出ることを先に確かめておくのがいい。0が出ない状態のまま運用に載せると、その出力は数日で読まれなくなる。

6. これから作るリポジトリで漏らさない

漏らさないための4段の設定と、条件付きインクルードの書き方

6-1. noreply アドレスをグローバル既定にする

GitHubは <ID>+<ユーザー名>@users.noreply.github.com という転送用アドレスを配っている。これを --global で入れておけば、リポジトリ側で設定を忘れても本物のアドレスは出ない(コミットメールアドレスの設定)。

git config --global user.email "12345678+username@users.noreply.github.com"
git config --global user.name  "username"

6-2. サーバー側でも弾く

設定画面で、コマンドラインからの push が個人メールを公開する場合にサーバー側で拒否するオプションを有効にできる(Blocking command line pushes that expose your personal email address)。ローカルの設定漏れに対する二重の網になる。

6-3. 設定されていないコミットを作らせない

個人的にはこれが一番効いた。

git config --global user.useConfigOnly true

ドキュメントの説明は「user.emailuser.name の既定値を推測しようとするのをやめ、設定からのみ値を取る」。つまりホスト名からアドレスを合成する経路が閉じるgit-config)。user.email が設定されていないリポジトリでコミットしようとすると、その場で設定を促されて止まる。前述のホスト名経由の漏れは、この1行で構造的に潰せる。

設定 効く範囲 防げるもの
user.email--global で noreply に これから作る全コミット 個人メールの露出
user.useConfigOnly true 設定漏れのリポジトリ ホスト名経由の露出
GitHub側の push ブロック サーバー側 ローカル設定を忘れた場合
公開前の全数検査 すでに存在する履歴 過去分の見落とし

7. 個人情報と秘密情報は別に守る

7-1. 秘密情報そのものはスキャンに任せる

APIキーやトークンについては、GitHubのシークレットスキャンが検出してくれる(about secret scanning)。ただし個人名やホスト名は「秘密情報」として定義されていないので、ここは自分で検査を持つ必要がある。守る対象が違うと割り切って、両方持つのが早い。

7-2. 「取り消せるもの」と「取り消せないもの」で分ける

この2つは、漏れたあとの打ち手がまったく違う。整理するとこうなる。

種類 漏れたあとの打ち手
取り消せる APIキー・トークン・パスワード 失効させて発行し直す。履歴に残っていても価値がゼロになる
取り消せない 本名・住所・電話番号・個人メール 失効という概念が無い。履歴から消すしかなく、消しきれない

秘密情報の事故は「鍵を回せば終わり」で、履歴の書き換えは後片付けにすぎない。個人情報の事故には、その「鍵を回す」に当たる操作が存在しない。だから同じ「漏洩対策」の言葉でまとめると判断を誤る。

自動検出の手厚さも、この2つでは正反対だ。取り消せるほうには立派なスキャンが用意されていて、取り消せないほうには何も無い。 自前の検査を持つ理由はここにある。

8. 出てしまったあとにできること

8-1. 履歴の書き換えは可能だが副作用が重い

公式の手順は git filter-repo を使ってローカルで履歴を書き換え、それでリモートを更新するというもの(Removing sensitive data from a repository / git-filter-repo。かつて案内されていた git filter-branch はドキュメント自身が非推奨としている)。

公式ドキュメントが挙げている副作用が、そのまま実務のコストになる。

  • コミットのSHAが全部変わる。PRの差分が別物になり、旧差分に付いたコメントが失効することがある
  • そのため、書き換え前に開いているPRはマージするか閉じておくことが推奨されている
  • 書き換え対象より前のコミット署名・タグ署名は失われる
  • クローンした全員との調整が要る

そして今回のように force-push がそもそも禁止されている環境だと、この手順自体が実行できない。「直せる」と「直せる環境にいる」は別だった。

8-2. 書き換えても消えたとは限らない

もう一つ現実的な話として、履歴を書き換えても、SHAを直接指定して到達できるキャッシュ済みのビューや、フォーク側に残ったコミットがある。公式ドキュメントが「キャッシュされたビューの削除は GitHub Support に連絡」という導線を用意していること自体が、自力では消しきれない領域があることを示している。

だから結論はこうなる。「消す」より「出さない」ほうが、コストが2桁違う。

8-3. 自分がどう判断したか

見つけた時点で公開を止めるべきか迷ったが、止めなかった。理由は3つ。

  1. これから作るコミットは noreply になるよう直した。新しい露出を足していない
  2. ファイル本文の検査は0件のまま。本文側には出ていない
  3. すでに公開済みの履歴は、いま作業を止めても1ミリも減らない

ただし**「直した」とは書かなかった**。未解決の申し送りとして記録に残し、force-push の可否を含めて別途扱うことにした。ここを曖昧にすると、次に読んだ人が「対応済み」と誤解して同じ穴を再生産する。報告の言葉づかいは、次の人の行動を決める。

9. まとめ

取り消せる秘密情報と、取り消せない個人情報で打ち手が違うことの対比

  1. 公開されるのは本文だけではない。コミットの著者欄・ブランチ名・ファイル名・メタデータ・Issue も公開物
  2. git log は今いるブランチしか見ない。判定は --all と、リモートの実体で行う
  3. user.email 未設定はホスト名経由で漏れる。macOSのホスト名にはアカウント名が入る
  4. user.useConfigOnly true で、設定漏れのコミットを構造的に止める
  5. user.email は最初のコミットより前に設定する。後から直しても既存分には効かない
  6. 書き換えは可能だが副作用が重く、消しきれない領域が残る。出さないほうが安い
  7. 検査を作るときは「これで公開物を全部カバーしているか」を1問自答する

自分の場合、ルールは書いてあった。検査も動いていた。それでも漏れた。足りなかったのはルールでも根性でもなく、検査の対象範囲の定義だった。

AIエージェントに開発を任せていると、コミットの数だけが静かに増えていく。1日に何十コミットも積まれる状況では、誰かが著者欄を目で見て気づくことは期待できない。エージェントを並走させたときに何が壊れたかはこちらの記事に書いた。

次にやることを1つだけ挙げるなら、第5章のスクリプトを自分のアカウントで流してみてほしい。数秒で終わる。0件で終われば何も失うものはないし、1件でも出たら、今日いちばん価値のある数秒になる。


「AIエージェントに開発を任せているが、公開前に何を機械で止めればいいか分からない」という相談を、監査と伴走の形で引き受けています → 制作・自動化のご相談(毎日ラボ)

本文中の数値は、すべて自分のアカウントでの実測値です(2026-09-06 時点)。リポジトリ名と検出された識別子そのものは、当然ながら記事には載せていません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?