From: "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Johannes Schindelin <johannes.schindelin@gmx.de>,
Johannes Schindelin <johannes.schindelin@gmx.de>
Subject: [PATCH 5/8] hook(clone protections): add escape hatch
Date: Fri, 17 May 2024 23:15:53 +0000 [thread overview]
Message-ID: <a4f5eeef6677462267d1dbf4b2b58d30aa013684.1715987756.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.1732.git.1715987756.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
next prev parent reply other threads:[~2024-05-17 23:16 UTC|newest]
Thread overview: 54+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-05-17 23:15 [PATCH 0/8] Various fixes for v2.45.1 and friends Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-18 0:10 ` Junio C Hamano
2024-05-18 18:58 ` Johannes Schindelin
2024-05-17 23:15 ` Johannes Schindelin via GitGitGadget [this message]
2024-05-18 0:21 ` [PATCH 5/8] hook(clone protections): add escape hatch Junio C Hamano
2024-05-17 23:15 ` [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
2024-05-18 0:20 ` Junio C Hamano
2024-05-17 23:15 ` [PATCH 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
2024-05-17 23:15 ` [PATCH 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-17 23:52 ` [PATCH 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-18 0:02 ` Johannes Schindelin
2024-05-18 10:32 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 1/8] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 2/8] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 3/8] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 5/8] hook(clone protections): add escape hatch Johannes Schindelin via GitGitGadget
2024-05-18 18:14 ` Jeff King
2024-05-18 18:54 ` Junio C Hamano
2024-05-18 19:35 ` Jeff King
2024-05-18 19:37 ` Johannes Schindelin
2024-05-18 19:32 ` Johannes Schindelin
2024-05-18 19:47 ` Jeff King
2024-05-18 20:06 ` Johannes Schindelin
2024-05-18 21:12 ` Jeff King
2024-05-19 1:15 ` Junio C Hamano
2024-05-20 16:05 ` Johannes Schindelin
2024-05-20 18:18 ` Junio C Hamano
2024-05-20 19:38 ` Johannes Schindelin
2024-05-20 20:07 ` Junio C Hamano
2024-05-20 21:03 ` Johannes Schindelin
2024-05-18 10:32 ` [PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation Johannes Schindelin via GitGitGadget
2024-05-18 10:32 ` [PATCH v2 8/8] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-18 17:07 ` [PATCH v2 0/8] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-18 19:22 ` Johannes Schindelin
2024-05-18 20:13 ` Johannes Schindelin
2024-05-20 20:21 ` [PATCH v3 0/6] " Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 1/6] hook: plug a new memory leak Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 2/6] init: use the correct path of the templates directory again Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 3/6] Revert "core.hooksPath: add some protection while cloning" Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 5/6] clone: drop the protections where hooks aren't run Johannes Schindelin via GitGitGadget
2024-05-20 20:22 ` [PATCH v3 6/6] Revert "Add a helper function to compare file contents" Johannes Schindelin via GitGitGadget
2024-05-20 23:56 ` [PATCH v3 0/6] Various fixes for v2.45.1 and friends Junio C Hamano
2024-05-21 5:33 ` Junio C Hamano
2024-05-21 18:14 ` Junio C Hamano
2024-05-21 22:33 ` brian m. carlson
2024-05-21 22:40 ` Junio C Hamano
2024-05-21 23:04 ` Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=a4f5eeef6677462267d1dbf4b2b58d30aa013684.1715987756.git.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=johannes.schindelin@gmx.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).