Git
GitHub
bitbucket
GitLab

git で一度 push した履歴は簡単には消えないが、ダウンロードも難しい

動機

リモートリポジトリに含めたくないコミット履歴をgit rebaseコマンドで無に帰す方法 等(TODO: 要アップデート)、git において過去の履歴を消す方法が挙げられている。これは本当に安全なのか。

疑問は


という意見による。

結論

結論を先に書くと、一度 push してしまうと、しばらくは(見つけにくいけれども)データが残る可能性がある。ということになった。ただし、cygwin 上のファイルシステムと、ssh 越しにしか実験をしていないため、GitHub, GitLab, bitbucket 等での扱いは別途調べる必要がある。

検証手法

とても長い…

概要

  1. サーバへのアップロード
    1. 本来隠したい情報を持つコミットを行う
    2. git push する # 時点 B
    3. 本来隠したい情報を消すコミットを加える
    4. git rebase で、本来隠したい情報のコミット(コミット自体の存在を)を消す
    5. git push -f する # 時点 C
    6. 別のリポジトリに git push する
  2. サーバからのダウンロード

サーバへのアップロード

SSH 越しサーバに対する実行例

サーバ側準備

mkdir -p git_test
cd git_test
git init --bare repo_bare.git
git init --bare repo_bare_2.git

履歴追加の流れ

git clone ssh://USER@example.com/~/git_test/repo_bare.git repo_a
## warning: You appear to have cloned an empty repository.

cd repo_a
touch test.txt
echo "init" >> test.txt
git add test.txt
git commit -m "Initial commit"
## [master (root-commit) c8c1fa6] Initial commit
##  1 file changed, 1 insertion(+)
##  create mode 100644 test.txt

# ここで、最初のコミットを push
git push
# この時点を A とする

echo "IMPORTANT_KEY" >> test.txt
git add test.txt
git commit -m "IMPORTANT_KEY"
## [master 12458d0] IMPORTANT_KEY
##  1 file changed, 1 insertion(+)

# ここで、問題のあるファイルを含むコミットを push
git push
# この時点を B とする

# KEY を消すような編集をコミット
echo "init" > test.txt
echo "KEY_REMOVED" >> test.txt
git add test.txt
git commit -m "KEY_REMOVED"
## [master 988ae1e] KEY_REMOVED
##  1 file changed, 1 insertion(+)

# ここで git push をすると、サーバ上で object が増えるが
# push していなくても大勢の結果は変わらないはず

# 最後の commit を squash する
git rebase -i "HEAD~2"
# 編集画面で
## pick 12458d0 IMPORTANT_KEY
## s 988ae1e KEY_REMOVED
# とし、保存。コミットメッセージは適当に。

## [detached HEAD b7864a0] KEY_REMOVED
##  Date: Thu Jan 11 20:36:46 2018 +0900
##  1 file changed, 1 insertion(+)
## Successfully rebased and updated refs/heads/master.

# 上書き
git push -f
# この時点を C とする

## Counting objects: 3, done.
## Writing objects: 100% (3/3), 273 bytes | 39.00 KiB/s, done.
## Total 3 (delta 0), reused 0 (delta 0)
## To ssh://example.com/~/git_test/repo_bare.git
##  + 12458d0...b7864a0 master -> master (forced update)

新しいリポジトリには push されないのを確認

git remote add repo_2 ssh://USER@example.com/~/git_test/repo_bare_2.git
git push repo_2
# この時点を D とする
## Counting objects: 6, done.
## Delta compression using up to 4 threads.
## Compressing objects: 100% (2/2), done.
## Writing objects: 100% (6/6), 461 bytes | 115.00 KiB/s, done.
## Total 6 (delta 0), reused 0 (delta 0)
## To ssh://example.com/~/git_test/repo_bare_2.git
##  * [new branch]      master -> master

実行結果を確認

時点 A でのサーバサイド

cd ~/git_test/repo_bare.git
find . -type f

すると

./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./hooks/pre-applypatch.sample
./hooks/pre-push.sample
./hooks/pre-commit.sample
./hooks/pre-rebase.sample
./hooks/post-update.sample
./hooks/applypatch-msg.sample
./hooks/update.sample
./hooks/prepare-commit-msg.sample
./hooks/commit-msg.sample
./HEAD
./info/exclude
./refs/heads/master
./description

となる。以下、hooks は邪魔なので載せない。

時点 B でのサーバサイド

時点 B では

./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
./objects/12/458d00bfda6ad83156238f0001f6a27d40c01d
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/8a/8368bda34f336c403d2cce0bda106796d45454
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description

である。いくつかオブジェクトが増えているが、減るものはない。抽出すると

13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
12/458d00bfda6ad83156238f0001f6a27d40c01d
8a/8368bda34f336c403d2cce0bda106796d45454

が増えている。これはそれぞれ(順不同)、時点 B での commit object, tree object, blob object である。内容は

git cat-file -p 8a83

のように確認でき、内容が

init
IMPORTANT_KEY

等と分かる。ここに、消したい情報が含まれている。

時点 C でのサーバサイド

時点 C では

./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/13/eaf4447e61e19b92fa7d707fd2ec909ed027f2
./objects/49/02ccbada222f23c21482ceae2b68c0922c80bd
./objects/b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
./objects/12/458d00bfda6ad83156238f0001f6a27d40c01d
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/8a/8368bda34f336c403d2cce0bda106796d45454
./objects/b7/864a0792da218c60d042078b9d7f90d76d3759
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description

である。

49/02ccbada222f23c21482ceae2b68c0922c80bd
b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
b7/864a0792da218c60d042078b9d7f90d76d3759

が増えている。これらは、rebase された後の commit object, tree object, blob object である。

8a/8368bda34f336c403d2cce0bda106796d45454 が消えていないことに注意しよう。つまり、サーバ管理者には、一度アップロードしたコミットの内容は丸見えとなる。

時点 D でのサーバサイド

cd ~/git_test/repo_bare_2.git
find . -type f
./objects/c8/c1fa69ff834dcb24f7007b38f6d01e25f48e3e
./objects/49/02ccbada222f23c21482ceae2b68c0922c80bd
./objects/b9/4c8f66fbc9ae8e205b2cae341f34ce75bc98ad
./objects/e1/29ad3ad5453f614c853331670e9f87156e9d40
./objects/b7/864a0792da218c60d042078b9d7f90d76d3759
./objects/b1/b716105590454bfc4c0247f193a04088f39c7f
./config
./HEAD
./info/exclude
./refs/heads/master
./description

である。(hook は除いている) コミットオブジェクトが少ない。8a/8368bda34f336c403d2cce0bda106796d45454 がないので、情報漏洩はない。

サーバからのダウンロード

ここからが難しい。時点 C で、サーバを直接覗けばファイルがあることは分かる。そのデータを手に入れることはできるのだろうか。

git clone ssh://USER@example.com/~/git_test/repo_bare.git repo_b

cd repo_b
git cat-file -p 8a83

すると、clone

Cloning into 'repo_b'...
Enter passphrase for key '/home/tako/.ssh/id_ed25519':
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.

の後、

fatal: Not a valid object name 8a83

となる。つまり、8a83... という object は入手できていない。つまり、第三者は簡単に入手できない。

しかし、「何らかの方法で入手できる可能性がある」場合、セキュリティ的に問題がないとは言えない。入手する方法はあるだろうか?

ここでは、ファイルそのものの中身は分からないが、「commit object や、tree object の id が分かってしまっている」という仮定を置こう。

$ git cat-file -p 1245
tree 13eaf4447e61e19b92fa7d707fd2ec909ed027f2
parent c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e
author XXX <test@example.com> 1515670606 +0900
committer XXX <test@example.com> 1515670606 +0900

IMPORTANT_KEY

$ git cat-file -p 13ea
100644 blob 8a8368bda34f336c403d2cce0bda106796d45454    test.txt

$ git cat-file -p 8a83
init
IMPORTANT_KEY

である。最初の commit id が 1245 のところは、環境によって異なる。

12458d00bfda6ad83156238f0001f6a27d40c01d
13eaf4447e61e19b92fa7d707fd2ec909ed027f2
8a8368bda34f336c403d2cce0bda106796d45454

さて、これを取得するために、fetch をしようとすると

$ git fetch origin 1245
fatal: Couldn't find remote ref 1245
fatal: The remote end hung up unexpectedly

となる。取得できない。

git には、fetch-pack という、object を取得する仕組みがある。これを利用して、オブジェクトの取得を試みる。

git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports ofs-delta
Server version is git/2.7.4
Marking b7864a0792da218c60d042078b9d7f90d76d3759 as complete
error: Server does not allow request for unadvertised object 12458d00bfda6ad83156238f0001f6a27d40c01d

取得できない。Server does not allow request for unadvertised object ということで、おそらく、ref から辿れない object は取得できない。

サーバ側で、git config uploadpack.allowtipsha1inwant true を実行する、または、config に

[uploadpack]
    allowtipsha1inwant = true

を書いておいてみる。

$ git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 8a8368bda34f336c403d2cce0bda106796d45454
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports allow-tip-sha1-in-want
Server supports ofs-delta
Server version is git/2.7.4
Marking b7864a0792da218c60d042078b9d7f90d76d3759 as complete
want 8a8368bda34f336c403d2cce0bda106796d45454 (8a8368bda34f336c403d2cce0bda106796d45454)
have b7864a0792da218c60d042078b9d7f90d76d3759
done
fatal: git upload-pack: not our ref 8a8368bda34f336c403d2cce0bda106796d45454
fatal: The remote end hung up unexpectedly

で、様相が変わる。なんと

git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 8a8368bda34f336c403d2cce0bda106796d45454

ができてしまう。

https://git-scm.com/docs/git-config を見ると、いろいろなオプションがあることが分かる。

git fetch-pack -v ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d

はいろいろ試してみて私の環境ではだめだったが、サーバ側で

git config uploadarchive.allowUnreachable true

すると

git archive --remote ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d

が応答を返すようになる。

git archive --remote ssh://USER@example.com/~/git_test/repo_bare.git 12458d00bfda6ad83156238f0001f6a27d40c01d
pax_global_header00006660000000000000000000000064132256461160014520gustar00rootroot0000000000000052 comment=12458d00bfda6ad83156238f0001f6a27d40c01d
test.txt000066400000000000000000000000231322564611600125770ustar00rootroot00000000000000init
IMPORTANT_KEY

という感じである。

ここまでの結論

object の sha1 つまり、commit id 等が分かっただけでは、ssh 接続で、git コマンドだけを使っている限りは、隠されたデータを取得することはできない。

ただし、サーバ側でオプション(config) を設定している場合はその限りではない。

github について

git プロトコルに git fetch-pack を利用

# PRIVATE_KEY
git fetch-pack -v git://github.com/USER/github_test.git 12458d00bfda6ad83156238f0001f6a27d40c01d

# Initial commit
git fetch-pack -v git://github.com/USER/github_test.git c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e

は、受信に失敗する。error: Server does not allow request for unadvertised object c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e 等と表示される。一方で、

# KEY_REMOVED
git fetch-pack -v git://github.com/USER/github_test.git b7864a0792da218c60d042078b9d7f90d76d3759

は成功する。何もオブジェクトを持っていない場合は

Looking up github.com ... done.
Connecting to github.com (port 9418) ... 192.30.255.113 done.
Server supports multi_ack_detailed
Server supports side-band-64k
Server supports ofs-delta
Server version is git/github-g7f1c57c193
want b7864a0792da218c60d042078b9d7f90d76d3759 (b7864a0792da218c60d042078b9d7f90d76d3759)
done
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), done.
b7864a0792da218c60d042078b9d7f90d76d3759 b7864a0792da218c60d042078b9d7f90d76d3759

等となる。

https プロトコル

アウトプットは長いので省略する。

# KEY_REMOVED
# OK
printf "0032want b7864a0792da218c60d042078b9d7f90d76d3759\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout

# PRIVATE_KEY
# NG
printf "0032want 12458d00bfda6ad83156238f0001f6a27d40c01d\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout

# Initial commit
# OK
printf "0032want c8c1fa69ff834dcb24f7007b38f6d01e25f48e3e\n00000032have 0000000000000000000000000000000000000000\n0009done\n"| curl -H "User-Agent: git/2.14.1" --data-binary @- "https://github.com/USER/github_test.git/git-upload-pack" -H "Content-Type: application/x-git-upload-pack-request" --trace-ascii /dev/stdout

ということで、参照するのが難しくなったコミットの PACK は取得できない。

サイトにオブジェクト id を指定してアクセス

ところが、これができてしまった。

https://github.com/USER/github_test/commit/12458d00bfda6ad83156238f0001f6a27d40c01d

(ダミーです)にアクセスできてしまった。よって、github はダメ。

https://qiita.com/dtan4/items/34e41e3bd40a43fd8cbf
にあるように、

実はコミット単体のページは GitHub 上にキャッシュとして存在しており、https://github.com/dtan4/terraforming/commit/64f752dd8d93c5b7326175a69cece9e742fd010b のようにコミットハッシュを直接指定すると普通に閲覧できます。

そこで、https://github.com/contact から GitHub のサポートにキャッシュを消すよう連絡する必要があります。

https, webdav について追記予定