TL;DR: サブモジュールは独立した Git リポジトリです。サブモジュール配下で origin を「自分が書き込める Public リポジトリ」に向けたまま push すると、その中身は即時に公開されます。さらに親リポジトリの push.recurseSubmodules や pre-push フックの実装次第では、親で git push しただけでサブモジュールが push され、意図せず漏えいが起こり得ます。
背景 モノレポやサイト構築(Hugo など)で外部テーマ/ライブラリを submodule として取り込むのは一般的です。しかし、サブモジュールは“別リポジトリ”そのものであり、配下で git remote set-url origin … を変えれば、単独で push できます。この性質と、親の push 時の挙動(push.recurseSubmodules、pre-push フック、CI スクリプト)が組み合わさると、気づかないうちに第三者の Public リポジトリへコードが押し出される危険があります。
よくある漏えいシナリオ flowchart TB A[開発者が親リポで作業] –> B[サブモジュール内で修正] B –> C[サブモジュール内の origin を自分の Public リポに変更] C –> D{親で git push} D –>|on-demand/フックが push| E[サブモジュールも push] E –> F[Public リポに中身が公開]
トリガーA: サブモジュール内で origin を書き込み可能な Public リポジトリに設定
トリガーB: 親で git push した際に
push.recurseSubmodules=on-demand で「参照コミットがリモートに無ければサブモジュールも push」
あるいは pre-push フックが git submodule foreach などで push を試みる
すぐできる被害最小化(即時ロック) 親リポジトリでサブモジュール push を止める:
親リポで: サブモジュールを push 対象にしない
git config push.recurseSubmodules no
push のドライラン癖付け
git push –dry-run origin main
各サブモジュールで push を物理的に無効化(安全策):
ルートで実行: すべてのサブモジュールの pushURL を無効化
(fetch URL はそのまま)
git config -f .gitmodules –get-regexp ‘^submodule..*.path$’ | while read -r _ path; do echo “Locking $path” git -C “$path” remote set-url –push origin DISABLED || true hookdir="$(git -C “$path” rev-parse –git-dir)/hooks" mkdir -p “$hookdir” cat > “$hookdir/pre-push” «‘SH’ #!/bin/sh echo “ERROR: Pushing from this submodule is disabled.” >&2 exit 1 SH chmod +x “$hookdir/pre-push” done
これでサブモジュール直下での git push は常に失敗し、誤操作やフック経由の push も止まります。
恒久対策(レイヤ別)
- 設定レイヤ(Git) 親リポ: git config push.recurseSubmodules no をデフォルトに
サブモジュール URL の整合性: git submodule sync –recursive
危険 URL のブロック(親の pre-push フック):
.git/hooks/pre-push (親リポ)
#!/bin/sh set -e url=$(git remote get-url –push origin 2>/dev/null || echo “”) case “$url” in git@github.com:your-org/|https://github.com/your-org/) ;; *) echo “ERROR: Blocked push to non-allowed remote: $url” >&2; exit 1;; esac
- リポジトリレイヤ(GitHub 側) Private 化(フォークやミラーも)
Protected Branch(強制 push 禁止、PR 必須、レビュー必須)
Secret Scanning / Push Protection を有効化
CI レイヤ(検知) GitHub Actionsで誤 push を検知・ブロック: name: guard-visibility on: [push] jobs: check: runs-on: ubuntu-latest steps:
- name: Fail if repo is public if: ${{ !github.event.repository.private }} run: | echo “Repository is public. Aborting.” >&2 exit 1
- uses: actions/checkout@v4
- name: Allowlist push target run: | url=$(git remote get-url –push origin || true) case “$url” in git@github.com:your-org/|https://github.com/your-org/) ;; *) echo “Blocked remote: $url” >&2; exit 1;; esac
運用レイヤ(人の習慣) git remote -v/git remote get-url –push origin をpush 前に確認
git push –dry-run を毎回実行
サブモジュールに変更を加える場合は 必ずフォークを Private で作る → .gitmodules をフォーク URL に変更 → PR
兆候と検知ポイント git push 時に 予期しないリモート URL がログに出る(例: 第三者のテーマ作者のリポ)
pre-push / CI ログに サブモジュールでの push 成功の痕跡
GitHub の 監査ログ/Contributors に意図しない push 履歴
調査ワンライナー:
親の push 先
git remote -v | sed -n ‘/origin/p’
すべてのサブモジュールの fetch/push 先一覧
git config -f .gitmodules –get-regexp ‘^submodule..*.path$’ | while read -r _ path; do echo “[$path]”; git -C “$path” remote -v; echo done
よくある質問(FAQ) Q. リポジトリが Public か Private かで差はある? A. 公開可否と書き込み権限は別です。Public でも あなたに書き込み権限がなければ push できず、403 で止まります。逆に Public で書き込める先に向いていれば、即座に誰でも読める状態になります。 Q. –no-verify を付けても止まらないのは? A. –no-verify は フックの実行抑止だけです。origin の向き先が第三者リポであれば素通りしてしまうため、URL ガード(pre-push 内の URL 判定)と pushURL の無効化 を合わせ技で使います。 Q. サブモジュールの “detached HEAD” は問題? A. サブモジュールとしては 正常です。問題は どこに push され得るか です。
ベストプラクティス:Read‑Only 運用 + PR フロー 結論: 外部のサブモジュールは read‑only に保ち、変更が必要になったら「別途 clone(またはフォーク)→ 修正 → 上流へ PR」。親リポには 参照コミットだけ を更新します。
- Read‑Only の基本設定(親とサブモジュール)
親: サブモジュールを push 対象にしない
git config push.recurseSubmodules no
サブモジュール側: push を物理的に無効化(誤操作防止)
git -C
URL の整合を保つ(.gitmodules → ローカルへ反映)
git submodule sync –recursive
.gitmodules は原典(upstream)URLのまま。サブモジュール直下では push しない運用にします。 2) 変更が必要になったときの手順(別途 clone → PR)
1) 自分の作業ディレクトリで、上流のリポジトリをフォーク or 直接 clone
※ 機微が混じる可能性があるならフォークは Private 推奨
例: フォークを clone
git clone git@github.com:
git remote add upstream https://github.com/
git checkout -b fix/your-change
… 変更 …
git commit -s -m “fix:
GitHub で origin→upstream への PR を作成
PR が upstream にマージ された後、親リポで参照コミットを更新します: cd /path/to/parent
サブモジュールで最新を取得
git -C
目的のコミット(またはタグ)へ移動
git -C
親にポインタ更新を記録
git add
※ .gitmodules に branch = main を設定していれば、git submodule update –remote
パッチ適用: 親レポに patches/ を置き、git apply/git am を CI/ローカルで当てる。サブモジュール自体は upstream のまま。
いずれの方法でも、サブモジュール配下からの push は不要です。
まとめ サブモジュールは独立リポ。origin が Public かつ書き込み可であれば即漏えい。
親の push.recurseSubmodules=no、サブモジュール pushURL 無効化、pre-push の URL Allowlist で 多層防御 を。
CI と運用ルールで最後の見張りを置いて、ヒューマンエラーを吸収する。
付録: チェックリスト(配布用) 親: git remote -v で push 先を確認
親: git config push.recurseSubmodules が no
サブモジュール: remote -v で pushURL が DISABLED(または Private)
親 .git/hooks/pre-push に URL Allowlist 実装
GitHub: Private + Protected Branch + Secret Scanning
CI: 可視性チェック + リモート Allowlist
運用: git push –dry-run を習慣化
本記事のサンプルスクリプトは 自己責任でお試しください。組織のポリシーに合わせて、許可ドメインやブランチ名、CI 条件を調整してください。