git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 5/8] hook(clone protections): add escape hatch

From
Johannes Schindelin via GitGitGadget <gitgitgadget@gmail.com>
Date
May 18, 2024, 10:32 UTC
Message-ID
<b841db8392ebd924d1893829a7e5e22240f1e9cf.1716028366.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.1732.v2.git.1716028366.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>

As defense-in-depth measures, v2.39.4 and friends leading up to v2.45.1 introduced code that detects when hooks have been installed during a `git clone`, which is indicative of a common attack vector with critical severity that allows Remote Code Execution.

There are legitimate use cases for such behavior, though, for example when those hooks stem from Git's own templates, which system administrators are at liberty to modify to enforce, say, commit message conventions. The git clone protections specifically add exceptions to allow for that.

Another legitimate use case that has been identified too late to be handled in these security bug-fix versions is Git LFS: It behaves somewhat similar to common attack vectors by writing a few hooks while running the `smudge` filter during a regular clone, which means that Git has no chance to know that the hooks are benign and e.g. the `post-checkout` hook can be safely executed as part of the clone operation.

To help Git LFS, and other tools behaving similarly (if there are any), let's add a new, multi-valued `safe.hook.sha256` config setting. Like the already-existing `safe.*` settings, it is ignored in repository-local configs, and it is interpreted as a list of SHA-256 checksums of hooks' contents that are safe to execute during a clone operation. Future Git LFS versions will need to write those entries at the same time they install the `smudge`/`clean` filters.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config/safe.txt |  6 ++++
 hook.c                        | 66 ++++++++++++++++++++++++++++++++---
 t/t1800-hook.sh               | 15 ++++++++
 3 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt
index bde7f31459b..69ee845be89 100644
--- a/Documentation/config/safe.txt
+++ b/Documentation/config/safe.txt
@@ -59,3 +59,9 @@ which id the original user has.
 If that is not what you would prefer and want git to only trust
 repositories that are owned by root instead, then you can remove
 the `SUDO_UID` variable from root's environment before invoking git.
+
+safe.hook.sha256::
+	The value is the SHA-256 of hooks that are considered to be safe
+	to run during a clone operation.
++
+Multiple values can be added via `git config --global --add`.
diff --git a/hook.c b/hook.c
index fc974cee1d8..a2479738451 100644
--- a/hook.c
+++ b/hook.c
@@ -2,6 +2,7 @@
 #include "hook.h"
 #include "run-command.h"
 #include "config.h"
+#include "strmap.h"
 
 static int identical_to_template_hook(const char *name, const char *path)
 {
@@ -29,11 +30,65 @@ static int identical_to_template_hook(const char *name, const char *path)
 	return ret;
 }
 
+static struct strset safe_hook_sha256s = STRSET_INIT;
+static int safe_hook_sha256s_initialized;
+
+static int get_sha256_of_file_contents(const char *path, char *sha256)
+{
+	struct strbuf sb = STRBUF_INIT;
+	int fd;
+	ssize_t res;
+
+	git_hash_ctx ctx;
+	const struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA256];
+	unsigned char hash[GIT_MAX_RAWSZ];
+
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return -1;
+	res = strbuf_read(&sb, fd, 400);
+	close(fd);
+	if (res < 0)
+		return -1;
+
+	algo->init_fn(&ctx);
+	algo->update_fn(&ctx, sb.buf, sb.len);
+	strbuf_release(&sb);
+	algo->final_fn(hash, &ctx);
+
+	hash_to_hex_algop_r(sha256, hash, algo);
+
+	return 0;
+}
+
+static int safe_hook_cb(const char *key, const char *value, void *d)
+{
+	struct strset *set = d;
+
+	if (value && !strcmp(key, "safe.hook.sha256"))
+		strset_add(set, value);
+
+	return 0;
+}
+
+static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)
+{
+	if (get_sha256_of_file_contents(path, sha256) < 0)
+		return 0;
+
+	if (!safe_hook_sha256s_initialized) {
+		safe_hook_sha256s_initialized = 1;
+		git_protected_config(safe_hook_cb, &safe_hook_sha256s);
+	}
+
+	return strset_contains(&safe_hook_sha256s, sha256);
+}
+
 const char *find_hook(const char *name)
 {
 	static struct strbuf path = STRBUF_INIT;
 
 	int found_hook;
+	char sha256[GIT_SHA256_HEXSZ + 1] = { '\0' };
 
 	strbuf_reset(&path);
 	strbuf_git_path(&path, "hooks/%s", name);
@@ -65,13 +120,14 @@ const char *find_hook(const char *name)
 		return NULL;
 	}
 	if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
-	    !identical_to_template_hook(name, path.buf))
+	    !identical_to_template_hook(name, path.buf) &&
+	    !is_hook_safe_during_clone(name, path.buf, sha256))
 		die(_("active `%s` hook found during `git clone`:\n\t%s\n"
 		      "For security reasons, this is disallowed by default.\n"
-		      "If this is intentional and the hook should actually "
-		      "be run, please\nrun the command again with "
-		      "`GIT_CLONE_PROTECTION_ACTIVE=false`"),
-		    name, path.buf);
+		      "If this is intentional and the hook is safe to run, "
+		      "please run the following command and try again:\n\n"
+		      "  git config --global --add safe.hook.sha256 %s"),
+		    name, path.buf, sha256);
 	return path.buf;
 }
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 2ef3579fa7c..0f74c9154d0 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -177,4 +177,19 @@ test_expect_success 'git hook run a hook with a bad shebang' '
 	test_cmp expect actual
 '
 
+test_expect_success '`safe.hook.sha256` and clone protections' '
+	git init safe-hook &&
+	write_script safe-hook/.git/hooks/pre-push <<-\EOF &&
+	echo "called hook" >safe-hook.log
+	EOF
+
+	test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push 2>err &&
+	cmd="$(grep "git config --global --add safe.hook.sha256 [0-9a-f]" err)" &&
+	eval "$cmd" &&
+	GIT_CLONE_PROTECTION_ACTIVE=true \
+		git -C safe-hook hook run pre-push &&
+	test "called hook" = "$(cat safe-hook/safe-hook.log)"
+'
+
 test_done
-- 
gitgitgadget
Previous: Johannes Schindelin via GitGitGadgetNext: Jeff King
Message 21 of 54 in “Various fixes for v2.45.1 and friends”
  1. 0/8 Various fixes for v2.45.1 and friendsJohannes Schindelin via GitGitGadget, May 17, 2024
  2. 1/8 hook: plug a new memory leakJohannes Schindelin via GitGitGadget, May 17, 2024
  3. 2/8 init: use the correct path of the templates directory againJohannes Schindelin via GitGitGadget, May 17, 2024
  4. 3/8 Revert "core.hooksPath: add some protection while cloning"Johannes Schindelin via GitGitGadget, May 17, 2024
  5. 4/8 tests: verify that `clone -c core.hooksPath=/dev/null` works againJohannes Schindelin via GitGitGadget, May 17, 2024
  6. Junio C HamanoMay 18, 2024
  7. Johannes SchindelinMay 18, 2024
  8. 5/8 hook(clone protections): add escape hatchJohannes Schindelin via GitGitGadget, May 17, 2024
  9. Junio C HamanoMay 18, 2024
  10. 6/8 hooks(clone protections): special-case current Git LFS hooksJohannes Schindelin via GitGitGadget, May 17, 2024
  11. Junio C HamanoMay 18, 2024
  12. 7/8 hooks(clone protections): simplify templates hooks validationJohannes Schindelin via GitGitGadget, May 17, 2024
  13. 8/8 Revert "Add a helper function to compare file contents"Johannes Schindelin via GitGitGadget, May 17, 2024
  14. Junio C HamanoMay 17, 2024
  15. Johannes SchindelinMay 18, 2024
  16. 0/8 Various fixes for v2.45.1 and friendsJohannes Schindelin via GitGitGadget, May 18, 2024
  17. 1/8 hook: plug a new memory leakJohannes Schindelin via GitGitGadget, May 18, 2024
  18. 2/8 init: use the correct path of the templates directory againJohannes Schindelin via GitGitGadget, May 18, 2024
  19. 3/8 Revert "core.hooksPath: add some protection while cloning"Johannes Schindelin via GitGitGadget, May 18, 2024
  20. 4/8 tests: verify that `clone -c core.hooksPath=/dev/null` works againJohannes Schindelin via GitGitGadget, May 18, 2024
  21. 5/8 hook(clone protections): add escape hatchJohannes Schindelin via GitGitGadget, May 18, 2024
  22. Jeff KingMay 18, 2024
  23. Junio C HamanoMay 18, 2024
  24. Jeff KingMay 18, 2024
  25. Johannes SchindelinMay 18, 2024
  26. Johannes SchindelinMay 18, 2024
  27. Jeff KingMay 18, 2024
  28. Johannes SchindelinMay 18, 2024
  29. Jeff KingMay 18, 2024
  30. Junio C HamanoMay 19, 2024
  31. Johannes SchindelinMay 20, 2024
  32. Junio C HamanoMay 20, 2024
  33. Johannes SchindelinMay 20, 2024
  34. Junio C HamanoMay 20, 2024
  35. Johannes SchindelinMay 20, 2024
  36. 6/8 hooks(clone protections): special-case current Git LFS hooksJohannes Schindelin via GitGitGadget, May 18, 2024
  37. 7/8 hooks(clone protections): simplify templates hooks validationJohannes Schindelin via GitGitGadget, May 18, 2024
  38. 8/8 Revert "Add a helper function to compare file contents"Johannes Schindelin via GitGitGadget, May 18, 2024
  39. Junio C HamanoMay 18, 2024
  40. Johannes SchindelinMay 18, 2024
  41. Johannes SchindelinMay 18, 2024
  42. 0/6 Various fixes for v2.45.1 and friendsJohannes Schindelin via GitGitGadget, May 20, 2024
  43. 1/6 hook: plug a new memory leakJohannes Schindelin via GitGitGadget, May 20, 2024
  44. 2/6 init: use the correct path of the templates directory againJohannes Schindelin via GitGitGadget, May 20, 2024
  45. 3/6 Revert "core.hooksPath: add some protection while cloning"Johannes Schindelin via GitGitGadget, May 20, 2024
  46. 4/6 tests: verify that `clone -c core.hooksPath=/dev/null` works againJohannes Schindelin via GitGitGadget, May 20, 2024
  47. 5/6 clone: drop the protections where hooks aren't runJohannes Schindelin via GitGitGadget, May 20, 2024
  48. 6/6 Revert "Add a helper function to compare file contents"Johannes Schindelin via GitGitGadget, May 20, 2024
  49. Junio C HamanoMay 20, 2024
  50. Junio C HamanoMay 21, 2024
  51. Junio C HamanoMay 21, 2024
  52. brian m. carlsonMay 21, 2024
  53. Junio C HamanoMay 21, 2024
  54. Junio C HamanoMay 21, 2024

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.