Skip to content

Commit 8db1e87

Browse files
committed
clone: prevent hooks from running during a clone
Critical security issues typically combine relatively common vulnerabilities such as case confusion in file paths with other weaknesses in order to raise the severity of the attack. One such weakness that has haunted the Git project in many a submodule-related CVE is that any hooks that are found are executed during a clone operation. Examples are the `post-checkout` and `fsmonitor` hooks. However, Git's design calls for hooks to be disabled by default, as only disabled example hooks are copied over from the templates in `<prefix>/share/git-core/templates/`. As a defense-in-depth measure, let's prevent those hooks from running. Obviously, administrators can choose to drop enabled hooks into the template directory, though, _and_ it is also possible to override `core.hooksPath`, in which case the new check needs to be disabled. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 584de0b commit 8db1e87

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

builtin/clone.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
908908
int err = 0, complete_refs_before_fetch = 1;
909909
int submodule_progress;
910910
int filter_submodules = 0;
911+
const char *template_dir;
912+
char *template_dir_dup = NULL;
911913

912914
struct transport_ls_refs_options transport_ls_refs_options =
913915
TRANSPORT_LS_REFS_OPTIONS_INIT;
@@ -927,6 +929,13 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
927929
usage_msg_opt(_("You must specify a repository to clone."),
928930
builtin_clone_usage, builtin_clone_options);
929931

932+
xsetenv("GIT_CLONE_PROTECTION_ACTIVE", "true", 0 /* allow user override */);
933+
template_dir = get_template_dir(option_template);
934+
if (*template_dir && !is_absolute_path(template_dir))
935+
template_dir = template_dir_dup =
936+
absolute_pathdup(template_dir);
937+
xsetenv("GIT_CLONE_TEMPLATE_DIR", template_dir, 1);
938+
930939
if (option_depth || option_since || option_not.nr)
931940
deepen = 1;
932941
if (option_single_branch == -1)
@@ -1074,7 +1083,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
10741083
}
10751084
}
10761085

1077-
init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,
1086+
init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,
10781087
INIT_DB_QUIET);
10791088

10801089
if (real_git_dir) {
@@ -1392,6 +1401,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
13921401
free(unborn_head);
13931402
free(dir);
13941403
free(path);
1404+
free(template_dir_dup);
13951405
UNLEAK(repo);
13961406
junk_mode = JUNK_LEAVE_ALL;
13971407

hook.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@
33
#include "run-command.h"
44
#include "config.h"
55

6+
static int identical_to_template_hook(const char *name, const char *path)
7+
{
8+
const char *env = getenv("GIT_CLONE_TEMPLATE_DIR");
9+
const char *template_dir = get_template_dir(env && *env ? env : NULL);
10+
struct strbuf template_path = STRBUF_INIT;
11+
int found_template_hook, ret;
12+
13+
strbuf_addf(&template_path, "%s/hooks/%s", template_dir, name);
14+
found_template_hook = access(template_path.buf, X_OK) >= 0;
15+
#ifdef STRIP_EXTENSION
16+
if (!found_template_hook) {
17+
strbuf_addstr(&template_path, STRIP_EXTENSION);
18+
found_template_hook = access(template_path.buf, X_OK) >= 0;
19+
}
20+
#endif
21+
if (!found_template_hook)
22+
return 0;
23+
24+
ret = do_files_match(template_path.buf, path);
25+
26+
strbuf_release(&template_path);
27+
return ret;
28+
}
29+
630
const char *find_hook(const char *name)
731
{
832
static struct strbuf path = STRBUF_INIT;
@@ -38,6 +62,14 @@ const char *find_hook(const char *name)
3862
}
3963
return NULL;
4064
}
65+
if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
66+
!identical_to_template_hook(name, path.buf))
67+
die(_("active `%s` hook found during `git clone`:\n\t%s\n"
68+
"For security reasons, this is disallowed by default.\n"
69+
"If this is intentional and the hook should actually "
70+
"be run, please\nrun the command again with "
71+
"`GIT_CLONE_PROTECTION_ACTIVE=false`"),
72+
name, path.buf);
4173
return path.buf;
4274
}
4375

t/t5601-clone.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,57 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe
771771
git clone --filter=blob:limit=0 "file://$(pwd)/server" client
772772
'
773773

774+
test_expect_success 'clone with init.templatedir runs hooks' '
775+
git init tmpl/hooks &&
776+
write_script tmpl/hooks/post-checkout <<-EOF &&
777+
echo HOOK-RUN >&2
778+
echo I was here >hook.run
779+
EOF
780+
git -C tmpl/hooks add . &&
781+
test_tick &&
782+
git -C tmpl/hooks commit -m post-checkout &&
783+
784+
test_when_finished "git config --global --unset init.templateDir || :" &&
785+
test_when_finished "git config --unset init.templateDir || :" &&
786+
(
787+
sane_unset GIT_TEMPLATE_DIR &&
788+
NO_SET_GIT_TEMPLATE_DIR=t &&
789+
export NO_SET_GIT_TEMPLATE_DIR &&
790+
791+
git -c core.hooksPath="$(pwd)/tmpl/hooks" \
792+
clone tmpl/hooks hook-run-hookspath 2>err &&
793+
! grep "active .* hook found" err &&
794+
test_path_is_file hook-run-hookspath/hook.run &&
795+
796+
git -c init.templateDir="$(pwd)/tmpl" \
797+
clone tmpl/hooks hook-run-config 2>err &&
798+
! grep "active .* hook found" err &&
799+
test_path_is_file hook-run-config/hook.run &&
800+
801+
git clone --template=tmpl tmpl/hooks hook-run-option 2>err &&
802+
! grep "active .* hook found" err &&
803+
test_path_is_file hook-run-option/hook.run &&
804+
805+
git config --global init.templateDir "$(pwd)/tmpl" &&
806+
git clone tmpl/hooks hook-run-global-config 2>err &&
807+
git config --global --unset init.templateDir &&
808+
! grep "active .* hook found" err &&
809+
test_path_is_file hook-run-global-config/hook.run &&
810+
811+
# clone ignores local `init.templateDir`; need to create
812+
# a new repository because we deleted `.git/` in the
813+
# `setup` test case above
814+
git init local-clone &&
815+
cd local-clone &&
816+
817+
git config init.templateDir "$(pwd)/../tmpl" &&
818+
git clone ../tmpl/hooks hook-run-local-config 2>err &&
819+
git config --unset init.templateDir &&
820+
! grep "active .* hook found" err &&
821+
test_path_is_missing hook-run-local-config/hook.run
822+
)
823+
'
824+
774825
. "$TEST_DIRECTORY"/lib-httpd.sh
775826
start_httpd
776827

0 commit comments

Comments
 (0)