From 150e6b0aedf57d224c3c49038c306477fa159886 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Apr 2024 13:30:26 +0200 Subject: builtin/clone: stop resolving symlinks when copying files When a user performs a local clone without `--no-local`, then we end up copying the source repository into the target repository directly. To optimize this even further, we try to hardlink files into place instead of copying data over, which helps both disk usage and speed. There is an important edge case in this context though, namely when we try to hardlink symlinks from the source repository into the target repository. Depending on both platform and filesystem the resulting behaviour here can be different: - On macOS and NetBSD, calling link(3P) with a symlink target creates a hardlink to the file pointed to by the symlink. - On Linux, calling link(3P) instead creates a hardlink to the symlink itself. To unify this behaviour, 36596fd2df (clone: better handle symlinked files at .git/objects/, 2019-07-10) introduced logic to resolve symlinks before we try to link(3P) files. Consequently, the new behaviour was to always create a hard link to the target of the symlink on all platforms. Eventually though, we figured out that following symlinks like this can cause havoc when performing a local clone of a malicious repository, which resulted in CVE-2022-39253. This issue was fixed via 6f054f9fb3 (builtin/clone.c: disallow `--local` clones with symlinks, 2022-07-28), by refusing symlinks in the source repository. But even though we now shouldn't ever link symlinks anymore, the code that resolves symlinks still exists. In the best case the code does not end up doing anything because there are no symlinks anymore. In the worst case though this can be abused by an adversary that rewrites the source file after it has been checked not to be a symlink such that it actually is a symlink when we call link(3P). Thus, it is still possible to recreate CVE-2022-39253 due to this time-of-check-time-of-use bug. Remove the call to `realpath()`. This doesn't yet address the actual vulnerability, which will be handled in a subsequent commit. Reported-by: Apple Product Security Signed-off-by: Patrick Steinhardt Signed-off-by: Johannes Schindelin --- builtin/clone.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'builtin') diff --git a/builtin/clone.c b/builtin/clone.c index 3c2ae31a55..073e6323d7 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -320,7 +320,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, int src_len, dest_len; struct dir_iterator *iter; int iter_status; - struct strbuf realpath = STRBUF_INIT; mkdir_if_missing(dest->buf, 0777); @@ -358,8 +357,7 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, if (unlink(dest->buf) && errno != ENOENT) die_errno(_("failed to unlink '%s'"), dest->buf); if (!option_no_hardlinks) { - strbuf_realpath(&realpath, src->buf, 1); - if (!link(realpath.buf, dest->buf)) + if (!link(src->buf, dest->buf)) continue; if (option_local > 0) die_errno(_("failed to create link '%s'"), dest->buf); @@ -373,8 +371,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, strbuf_setlen(src, src_len); die(_("failed to iterate over '%s'"), src->buf); } - - strbuf_release(&realpath); } static void clone_local(const char *src_repo, const char *dest_repo) -- cgit v1.2.3 From d1bb66a546b4bb46005d17ba711caaad26f26c1e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Apr 2024 13:30:31 +0200 Subject: builtin/clone: abort when hardlinked source and target file differ When performing local clones with hardlinks we refuse to copy source files which are symlinks as a mitigation for CVE-2022-39253. This check can be raced by an adversary though by changing the file to a symlink after we have checked it. Fix the issue by checking whether the hardlinked destination file matches the source file and abort in case it doesn't. This addresses CVE-2024-32021. Reported-by: Apple Product Security Suggested-by: Linus Torvalds Signed-off-by: Patrick Steinhardt Signed-off-by: Johannes Schindelin --- builtin/clone.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) (limited to 'builtin') diff --git a/builtin/clone.c b/builtin/clone.c index 073e6323d7..4b80fa0870 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -357,8 +357,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, if (unlink(dest->buf) && errno != ENOENT) die_errno(_("failed to unlink '%s'"), dest->buf); if (!option_no_hardlinks) { - if (!link(src->buf, dest->buf)) + if (!link(src->buf, dest->buf)) { + struct stat st; + + /* + * Sanity-check whether the created hardlink + * actually links to the expected file now. This + * catches time-of-check-time-of-use bugs in + * case the source file was meanwhile swapped. + */ + if (lstat(dest->buf, &st)) + die(_("hardlink cannot be checked at '%s'"), dest->buf); + if (st.st_mode != iter->st.st_mode || + st.st_ino != iter->st.st_ino || + st.st_dev != iter->st.st_dev || + st.st_size != iter->st.st_size || + st.st_uid != iter->st.st_uid || + st.st_gid != iter->st.st_gid) + die(_("hardlink different from source at '%s'"), dest->buf); + continue; + } if (option_local > 0) die_errno(_("failed to create link '%s'"), dest->buf); option_no_hardlinks = 1; -- cgit v1.2.3 From 1204e1a824c34071019fe106348eaa6d88f9528d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 15 Apr 2024 13:30:41 +0200 Subject: builtin/clone: refuse local clones of unsafe repositories When performing a local clone of a repository we end up either copying or hardlinking the source repository into the target repository. This is significantly more performant than if we were to use git-upload-pack(1) and git-fetch-pack(1) to create the new repository and preserves both disk space and compute time. Unfortunately though, performing such a local clone of a repository that is not owned by the current user is inherently unsafe: - It is possible that source files get swapped out underneath us while we are copying or hardlinking them. While we do perform some checks here to assert that we hardlinked the expected file, they cannot reliably thwart time-of-check-time-of-use (TOCTOU) style races. It is thus possible for an adversary to make us copy or hardlink unexpected files into the target directory. Ideally, we would address this by starting to use openat(3P), fstatat(3P) and friends. Due to platform compatibility with Windows we cannot easily do that though. Furthermore, the scope of these fixes would likely be quite broad and thus not fit for an embargoed security release. - Even if we handled TOCTOU-style races perfectly, hardlinking files owned by a different user into the target repository is not a good idea in general. It is possible for an adversary to rewrite those files to contain whatever data they want even after the clone has completed. Address these issues by completely refusing local clones of a repository that is not owned by the current user. This reuses our existing infra we have in place via `ensure_valid_ownership()` and thus allows a user to override the safety guard by adding the source repository path to the "safe.directory" configuration. This addresses CVE-2024-32020. Signed-off-by: Patrick Steinhardt Signed-off-by: Johannes Schindelin --- builtin/clone.c | 14 ++++++++++++++ t/t0033-safe-directory.sh | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+) (limited to 'builtin') diff --git a/builtin/clone.c b/builtin/clone.c index 4b80fa0870..9ec500d427 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -321,6 +321,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, struct dir_iterator *iter; int iter_status; + /* + * Refuse copying directories by default which aren't owned by us. The + * code that performs either the copying or hardlinking is not prepared + * to handle various edge cases where an adversary may for example + * racily swap out files for symlinks. This can cause us to + * inadvertently use the wrong source file. + * + * Furthermore, even if we were prepared to handle such races safely, + * creating hardlinks across user boundaries is an inherently unsafe + * operation as the hardlinked files can be rewritten at will by the + * potentially-untrusted user. We thus refuse to do so by default. + */ + die_upon_dubious_ownership(NULL, NULL, src_repo); + mkdir_if_missing(dest->buf, 0777); iter = dir_iterator_begin(src->buf, DIR_ITERATOR_PEDANTIC); diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh index dc3496897a..11c3e8f28e 100755 --- a/t/t0033-safe-directory.sh +++ b/t/t0033-safe-directory.sh @@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' ' git status ' +test_expect_success 'local clone of unowned repo refused in unsafe directory' ' + test_when_finished "rm -rf source" && + git init source && + ( + sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER && + test_commit -C source initial + ) && + test_must_fail git clone --local source target && + test_path_is_missing target +' + +test_expect_success 'local clone of unowned repo accepted in safe directory' ' + test_when_finished "rm -rf source" && + git init source && + ( + sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER && + test_commit -C source initial + ) && + test_must_fail git clone --local source target && + git config --global --add safe.directory "$(pwd)/source/.git" && + git clone --local source target && + test_path_is_dir target +' + test_done -- cgit v1.2.3 From 7b70e9efb18c2cc3f219af399bd384c5801ba1d7 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 16 Apr 2024 04:35:33 -0400 Subject: upload-pack: disable lazy-fetching by default The upload-pack command tries to avoid trusting the repository in which it's run (e.g., by not running any hooks and not using any config that contains arbitrary commands). But if the server side of a fetch or a clone is a partial clone, then either upload-pack or its child pack-objects may run a lazy "git fetch" under the hood. And it is very easy to convince fetch to run arbitrary commands. The "server" side can be a local repository owned by someone else, who would be able to configure commands that are run during a clone with the current user's permissions. This issue has been designated CVE-2024-32004. The fix in this commit's parent helps in this scenario, as well as in related scenarios using SSH to clone, where the untrusted .git directory is owned by a different user id. But if you received one as a zip file, on a USB stick, etc, it may be owned by your user but still untrusted. This has been designated CVE-2024-32465. To mitigate the issue more completely, let's disable lazy fetching entirely during `upload-pack`. While fetching from a partial repository should be relatively rare, it is certainly not an unreasonable workflow. And thus we need to provide an escape hatch. This commit works by respecting a GIT_NO_LAZY_FETCH environment variable (to skip the lazy-fetch), and setting it in upload-pack, but only when the user has not already done so (which gives us the escape hatch). The name of the variable is specifically chosen to match what has already been added in 'master' via e6d5479e7a (git: extend --no-lazy-fetch to work across subprocesses, 2024-02-27). Since we're building this fix as a backport for older versions, we could cherry-pick that patch and its earlier steps. However, we don't really need the niceties (like a "--no-lazy-fetch" option) that it offers. By using the same name, everything should just work when the two are eventually merged, but here are a few notes: - the blocking of the fetch in e6d5479e7a is incomplete! It sets fetch_if_missing to 0 when we setup the repository variable, but that isn't enough. pack-objects in particular will call prefetch_to_pack() even if that variable is 0. This patch by contrast checks the environment variable at the lowest level before we call the lazy fetch, where we can be sure to catch all code paths. Possibly the setting of fetch_if_missing from e6d5479e7a can be reverted, but it may be useful to have. For example, some code may want to use that flag to change behavior before it gets to the point of trying to start the fetch. At any rate, that's all outside the scope of this patch. - there's documentation for GIT_NO_LAZY_FETCH in e6d5479e7a. We can live without that here, because for the most part the user shouldn't need to set it themselves. The exception is if they do want to override upload-pack's default, and that requires a separate documentation section (which is added here) - it would be nice to use the NO_LAZY_FETCH_ENVIRONMENT macro added by e6d5479e7a, but those definitions have moved from cache.h to environment.h between 2.39.3 and master. I just used the raw string literals, and we can replace them with the macro once this topic is merged to master. At least with respect to CVE-2024-32004, this does render this commit's parent commit somewhat redundant. However, it is worth retaining that commit as defense in depth, and because it may help other issues (e.g., symlink/hardlink TOCTOU races, where zip files are not really an interesting attack vector). The tests in t0411 still pass, but now we have _two_ mechanisms ensuring that the evil command is not run. Let's beef up the existing ones to check that they failed for the expected reason, that we refused to run upload-pack at all with an alternate user id. And add two new ones for the same-user case that both the restriction and its escape hatch. Signed-off-by: Jeff King Signed-off-by: Johannes Schindelin --- Documentation/git-upload-pack.txt | 16 ++++++++++++++++ builtin/upload-pack.c | 2 ++ promisor-remote.c | 10 ++++++++++ t/t0411-clone-from-partial.sh | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+) (limited to 'builtin') diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt index b656b47567..fc4c62d7bc 100644 --- a/Documentation/git-upload-pack.txt +++ b/Documentation/git-upload-pack.txt @@ -55,6 +55,22 @@ ENVIRONMENT admins may need to configure some transports to allow this variable to be passed. See the discussion in linkgit:git[1]. +`GIT_NO_LAZY_FETCH`:: + When cloning or fetching from a partial repository (i.e., one + itself cloned with `--filter`), the server-side `upload-pack` + may need to fetch extra objects from its upstream in order to + complete the request. By default, `upload-pack` will refuse to + perform such a lazy fetch, because `git fetch` may run arbitrary + commands specified in configuration and hooks of the source + repository (and `upload-pack` tries to be safe to run even in + untrusted `.git` directories). ++ +This is implemented by having `upload-pack` internally set the +`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it +(because you are fetching from a partial clone, and you are sure +you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to +`0`. + SEE ALSO -------- linkgit:gitnamespaces[7] diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c index 25b69da2bf..f446ff04f6 100644 --- a/builtin/upload-pack.c +++ b/builtin/upload-pack.c @@ -35,6 +35,8 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix) packet_trace_identity("upload-pack"); read_replace_refs = 0; + /* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */ + xsetenv("GIT_NO_LAZY_FETCH", "1", 0); argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0); diff --git a/promisor-remote.c b/promisor-remote.c index faa7612941..550a38f752 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -20,6 +20,16 @@ static int fetch_objects(struct repository *repo, int i; FILE *child_in; + /* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */ + if (git_env_bool("GIT_NO_LAZY_FETCH", 0)) { + static int warning_shown; + if (!warning_shown) { + warning_shown = 1; + warning(_("lazy fetching disabled; some objects may not be available")); + } + return -1; + } + child.git_cmd = 1; child.in = -1; if (repo != the_repository) diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh index eb3360dbca..b3d6ddc4bc 100755 --- a/t/t0411-clone-from-partial.sh +++ b/t/t0411-clone-from-partial.sh @@ -28,6 +28,7 @@ test_expect_success 'local clone must not fetch from promisor remote and execute test_must_fail git clone \ --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ evil clone1 2>err && + grep "detected dubious ownership" err && ! grep "fake-upload-pack running" err && test_path_is_missing script-executed ' @@ -37,6 +38,7 @@ test_expect_success 'clone from file://... must not fetch from promisor remote a test_must_fail git clone \ --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ "file://$(pwd)/evil" clone2 2>err && + grep "detected dubious ownership" err && ! grep "fake-upload-pack running" err && test_path_is_missing script-executed ' @@ -46,6 +48,7 @@ test_expect_success 'fetch from file://... must not fetch from promisor remote a test_must_fail git fetch \ --upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \ "file://$(pwd)/evil" 2>err && + grep "detected dubious ownership" err && ! grep "fake-upload-pack running" err && test_path_is_missing script-executed ' @@ -57,4 +60,19 @@ test_expect_success 'pack-objects should fetch from promisor remote and execute test_path_is_file script-executed ' +test_expect_success 'clone from promisor remote does not lazy-fetch by default' ' + rm -f script-executed && + test_must_fail git clone evil no-lazy 2>err && + grep "lazy fetching disabled" err && + test_path_is_missing script-executed +' + +test_expect_success 'promisor lazy-fetching can be re-enabled' ' + rm -f script-executed && + test_must_fail env GIT_NO_LAZY_FETCH=0 \ + git clone evil lazy-ok 2>err && + grep "fake-upload-pack running" err && + test_path_is_file script-executed +' + test_done -- cgit v1.2.3 From 9cf85473209ea8ae2b56c13145c4704d12ee1374 Mon Sep 17 00:00:00 2001 From: Filip Hejsek Date: Sun, 28 Jan 2024 05:09:17 +0100 Subject: clone: prevent clashing git dirs when cloning submodule in parallel While it is expected to have several git dirs within the `.git/modules/` tree, it is important that they do not interfere with each other. For example, if one submodule was called "captain" and another submodule "captain/hooks", their respective git dirs would clash, as they would be located in `.git/modules/captain/` and `.git/modules/captain/hooks/`, respectively, i.e. the latter's files could clash with the actual Git hooks of the former. To prevent these clashes, and in particular to prevent hooks from being written and then executed as part of a recursive clone, we introduced checks as part of the fix for CVE-2019-1387 in a8dee3ca61 (Disallow dubiously-nested submodule git directories, 2019-10-01). It is currently possible to bypass the check for clashing submodule git dirs in two ways: 1. parallel cloning 2. checkout --recurse-submodules Let's check not only before, but also after parallel cloning (and before checking out the submodule), that the git dir is not clashing with another one, otherwise fail. This addresses the parallel cloning issue. As to the parallel checkout issue: It requires quite a few manual steps to create clashing git dirs because Git itself would refuse to initialize the inner one, as demonstrated by the test case. Nevertheless, let's teach the recursive checkout (namely, the `submodule_move_head()` function that is used by the recursive checkout) to be careful to verify that it does not use a clashing git dir, and if it does, disable it (by deleting the `HEAD` file so that subsequent Git calls won't recognize it as a git dir anymore). Note: The parallel cloning test case contains a `cat err` that proved to be highly useful when analyzing the racy nature of the operation (the operation can fail with three different error messages, depending on timing), and was left on purpose to ease future debugging should the need arise. Signed-off-by: Filip Hejsek Signed-off-by: Johannes Schindelin --- builtin/submodule--helper.c | 17 +++++++++++++++++ submodule.c | 17 +++++++++++++++++ t/t7450-bad-git-dotfiles.sh | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) (limited to 'builtin') diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 6743fb27bd..b76e13ddce 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1717,6 +1717,23 @@ static int clone_submodule(const struct module_clone_data *clone_data, free(path); } + /* + * We already performed this check at the beginning of this function, + * before cloning the objects. This tries to detect racy behavior e.g. + * in parallel clones, where another process could easily have made the + * gitdir nested _after_ it was created. + * + * To prevent further harm coming from this unintentionally-nested + * gitdir, let's disable it by deleting the `HEAD` file. + */ + if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) { + char *head = xstrfmt("%s/HEAD", sm_gitdir); + unlink(head); + free(head); + die(_("refusing to create/use '%s' in another submodule's " + "git dir"), sm_gitdir); + } + connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0); p = git_pathdup_submodule(clone_data_path, "config"); diff --git a/submodule.c b/submodule.c index fae24ef34a..71ec23ad98 100644 --- a/submodule.c +++ b/submodule.c @@ -2146,10 +2146,27 @@ int submodule_move_head(const char *path, if (old_head) { if (!submodule_uses_gitfile(path)) absorb_git_dir_into_superproject(path); + else { + char *dotgit = xstrfmt("%s/.git", path); + char *git_dir = xstrdup(read_gitfile(dotgit)); + + free(dotgit); + if (validate_submodule_git_dir(git_dir, + sub->name) < 0) + die(_("refusing to create/use '%s' in " + "another submodule's git dir"), + git_dir); + free(git_dir); + } } else { struct strbuf gitdir = STRBUF_INIT; submodule_name_to_gitdir(&gitdir, the_repository, sub->name); + if (validate_submodule_git_dir(gitdir.buf, + sub->name) < 0) + die(_("refusing to create/use '%s' in another " + "submodule's git dir"), + gitdir.buf); connect_work_tree_and_git_dir(path, gitdir.buf, 0); strbuf_release(&gitdir); diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh index ba1f569bcb..8f94129e74 100755 --- a/t/t7450-bad-git-dotfiles.sh +++ b/t/t7450-bad-git-dotfiles.sh @@ -292,7 +292,7 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' ' fi ' -test_expect_success 'git dirs of sibling submodules must not be nested' ' +test_expect_success 'setup submodules with nested git dirs' ' git init nested && test_commit -C nested nested && ( @@ -310,9 +310,39 @@ test_expect_success 'git dirs of sibling submodules must not be nested' ' git add .gitmodules thing1 thing2 && test_tick && git commit -m nested - ) && + ) +' + +test_expect_success 'git dirs of sibling submodules must not be nested' ' test_must_fail git clone --recurse-submodules nested clone 2>err && test_i18ngrep "is inside git dir" err ' +test_expect_success 'submodule git dir nesting detection must work with parallel cloning' ' + test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err && + cat err && + grep -E "(already exists|is inside git dir|not a git repository)" err && + { + test_path_is_missing .git/modules/hippo/HEAD || + test_path_is_missing .git/modules/hippo/hooks/HEAD + } +' + +test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' ' + git clone nested nested_checkout && + ( + cd nested_checkout && + git submodule init && + git submodule update thing1 && + mkdir -p .git/modules/hippo/hooks/refs && + mkdir -p .git/modules/hippo/hooks/objects/info && + echo "../../../../objects" >.git/modules/hippo/hooks/objects/info/alternates && + echo "ref: refs/heads/master" >.git/modules/hippo/hooks/HEAD + ) && + test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err && + cat err && + grep "is inside git dir" err && + test_path_is_missing nested_checkout/thing2/.git +' + test_done -- cgit v1.2.3 From 97065761333fd62db1912d81b489db938d8c991d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 22 Mar 2024 11:19:22 +0100 Subject: submodules: submodule paths must not contain symlinks When creating a submodule path, we must be careful not to follow symbolic links. Otherwise we may follow a symbolic link pointing to a gitdir (which are valid symbolic links!) e.g. while cloning. On case-insensitive filesystems, however, we blindly replace a directory that has been created as part of the `clone` operation with a symlink when the path to the latter differs only in case from the former's path. Let's simply avoid this situation by expecting not ever having to overwrite any existing file/directory/symlink upon cloning. That way, we won't even replace a directory that we just created. This addresses CVE-2024-32002. Reported-by: Filip Hejsek Signed-off-by: Johannes Schindelin --- builtin/submodule--helper.c | 35 +++++++++++++++++++++++++++++++++ t/t7406-submodule-update.sh | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) (limited to 'builtin') diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index b76e13ddce..4c1a7dbcda 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1641,12 +1641,35 @@ static char *clone_submodule_sm_gitdir(const char *name) return sm_gitdir; } +static int dir_contains_only_dotgit(const char *path) +{ + DIR *dir = opendir(path); + struct dirent *e; + int ret = 1; + + if (!dir) + return 0; + + e = readdir_skip_dot_and_dotdot(dir); + if (!e) + ret = 0; + else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) || + (e = readdir_skip_dot_and_dotdot(dir))) { + error("unexpected item '%s' in '%s'", e->d_name, path); + ret = 0; + } + + closedir(dir); + return ret; +} + static int clone_submodule(const struct module_clone_data *clone_data, struct string_list *reference) { char *p; char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name); char *sm_alternate = NULL, *error_strategy = NULL; + struct stat st; struct child_process cp = CHILD_PROCESS_INIT; const char *clone_data_path = clone_data->path; char *to_free = NULL; @@ -1660,6 +1683,10 @@ static int clone_submodule(const struct module_clone_data *clone_data, "git dir"), sm_gitdir); if (!file_exists(sm_gitdir)) { + if (clone_data->require_init && !stat(clone_data_path, &st) && + !is_empty_dir(clone_data_path)) + die(_("directory not empty: '%s'"), clone_data_path); + if (safe_create_leading_directories_const(sm_gitdir) < 0) die(_("could not create directory '%s'"), sm_gitdir); @@ -1704,6 +1731,14 @@ static int clone_submodule(const struct module_clone_data *clone_data, if(run_command(&cp)) die(_("clone of '%s' into submodule path '%s' failed"), clone_data->url, clone_data_path); + + if (clone_data->require_init && !stat(clone_data_path, &st) && + !dir_contains_only_dotgit(clone_data_path)) { + char *dot_git = xstrfmt("%s/.git", clone_data_path); + unlink(dot_git); + free(dot_git); + die(_("directory not empty: '%s'"), clone_data_path); + } } else { char *path; diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index f094e3d7f3..63c24f7f7c 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -1179,4 +1179,52 @@ test_expect_success 'submodule update --recursive skip submodules with strategy= test_cmp expect.err actual.err ' +test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \ + 'submodule paths must not follow symlinks' ' + + # This is only needed because we want to run this in a self-contained + # test without having to spin up an HTTP server; However, it would not + # be needed in a real-world scenario where the submodule is simply + # hosted on a public site. + test_config_global protocol.file.allow always && + + # Make sure that Git tries to use symlinks on Windows + test_config_global core.symlinks true && + + tell_tale_path="$PWD/tell.tale" && + git init hook && + ( + cd hook && + mkdir -p y/hooks && + write_script y/hooks/post-checkout <<-EOF && + echo HOOK-RUN >&2 + echo hook-run >"$tell_tale_path" + EOF + git add y/hooks/post-checkout && + test_tick && + git commit -m post-checkout + ) && + + hook_repo_path="$(pwd)/hook" && + git init captain && + ( + cd captain && + git submodule add --name x/y "$hook_repo_path" A/modules/x && + test_tick && + git commit -m add-submodule && + + printf .git >dotgit.txt && + git hash-object -w --stdin dot-git.hash && + printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info && + git update-index --index-info err && + grep "directory not empty" err && + test_path_is_missing "$tell_tale_path" +' + test_done -- cgit v1.2.3 From eafffd9ad417bdf0a3c63e5276d5a18f563cd291 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 12 Apr 2024 21:00:44 +0200 Subject: clone_submodule: avoid using `access()` on directories In 0060fd1511b (clone --recurse-submodules: prevent name squatting on Windows, 2019-09-12), I introduced code to verify that a git dir either does not exist, or is at least empty, to fend off attacks where an inadvertently (and likely maliciously) pre-populated git dir would be used while cloning submodules recursively. The logic used `access(, X_OK)` to verify that a directory exists before calling `is_empty_dir()` on it. That is a curious way to check for a directory's existence and might well fail for unwanted reasons. Even the original author (it was I ;-) ) struggles to explain why this function was used rather than `stat()`. This code was _almost_ copypastad in the previous commit, but that `access()` call was caught during review. Let's use `stat()` instead also in the code that was almost copied verbatim. Let's not use `lstat()` because in the unlikely event that somebody snuck a symbolic link in, pointing to a crafted directory, we want to verify that that directory is empty. Signed-off-by: Johannes Schindelin --- builtin/submodule--helper.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'builtin') diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 4c1a7dbcda..9eacc43574 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1742,7 +1742,7 @@ static int clone_submodule(const struct module_clone_data *clone_data, } else { char *path; - if (clone_data->require_init && !access(clone_data_path, X_OK) && + if (clone_data->require_init && !stat(clone_data_path, &st) && !is_empty_dir(clone_data_path)) die(_("directory not empty: '%s'"), clone_data_path); if (safe_create_leading_directories_const(clone_data_path) < 0) -- cgit v1.2.3 From e8d0608944486019ea0e1ed2ed29776811a565c2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 26 Mar 2024 14:37:25 +0100 Subject: submodule: require the submodule path to contain directories only Submodules are stored in subdirectories of their superproject. When these subdirectories have been replaced with symlinks by a malicious actor, all kinds of mayhem can be caused. This _should_ not be possible, but many CVEs in the past showed that _when_ possible, it allows attackers to slip in code that gets executed during, say, a `git clone --recursive` operation. Let's add some defense-in-depth to disallow submodule paths to have anything except directories in them. Signed-off-by: Johannes Schindelin --- builtin/submodule--helper.c | 32 ++++++++++++++++++- submodule.c | 72 +++++++++++++++++++++++++++++++++++++++++++ submodule.h | 5 +++ t/t7423-submodule-symlinks.sh | 9 +++--- 4 files changed, 113 insertions(+), 5 deletions(-) (limited to 'builtin') diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 9eacc43574..941afe1568 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -294,6 +294,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item, struct child_process cp = CHILD_PROCESS_INIT; char *displaypath; + if (validate_submodule_path(path) < 0) + exit(128); + displaypath = get_submodule_displaypath(path, info->prefix); sub = submodule_from_path(the_repository, null_oid(), path); @@ -620,6 +623,9 @@ static void status_submodule(const char *path, const struct object_id *ce_oid, .free_removed_argv_elements = 1, }; + if (validate_submodule_path(path) < 0) + exit(128); + if (!submodule_from_path(the_repository, null_oid(), path)) die(_("no submodule mapping found in .gitmodules for path '%s'"), path); @@ -1220,6 +1226,9 @@ static void sync_submodule(const char *path, const char *prefix, if (!is_submodule_active(the_repository, path)) return; + if (validate_submodule_path(path) < 0) + exit(128); + sub = submodule_from_path(the_repository, null_oid(), path); if (sub && sub->url) { @@ -1360,6 +1369,9 @@ static void deinit_submodule(const char *path, const char *prefix, struct strbuf sb_config = STRBUF_INIT; char *sub_git_dir = xstrfmt("%s/.git", path); + if (validate_submodule_path(path) < 0) + exit(128); + sub = submodule_from_path(the_repository, null_oid(), path); if (!sub || !sub->name) @@ -1674,6 +1686,9 @@ static int clone_submodule(const struct module_clone_data *clone_data, const char *clone_data_path = clone_data->path; char *to_free = NULL; + if (validate_submodule_path(clone_data_path) < 0) + exit(128); + if (!is_absolute_path(clone_data->path)) clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(), clone_data->path); @@ -2542,6 +2557,9 @@ static int update_submodule(struct update_data *update_data) { int ret; + if (validate_submodule_path(update_data->sm_path) < 0) + return -1; + ret = determine_submodule_update_strategy(the_repository, update_data->just_cloned, update_data->sm_path, @@ -2649,12 +2667,21 @@ static int update_submodules(struct update_data *update_data) for (i = 0; i < suc.update_clone_nr; i++) { struct update_clone_data ucd = suc.update_clone[i]; - int code; + int code = 128; oidcpy(&update_data->oid, &ucd.oid); update_data->just_cloned = ucd.just_cloned; update_data->sm_path = ucd.sub->path; + /* + * Verify that the submodule path does not contain any + * symlinks; if it does, it might have been tampered with. + * TODO: allow exempting it via + * `safe.submodule.path` or something + */ + if (validate_submodule_path(update_data->sm_path) < 0) + goto fail; + code = ensure_core_worktree(update_data->sm_path); if (code) goto fail; @@ -3361,6 +3388,9 @@ static int module_add(int argc, const char **argv, const char *prefix) normalize_path_copy(add_data.sm_path, add_data.sm_path); strip_dir_trailing_slashes(add_data.sm_path); + if (validate_submodule_path(add_data.sm_path) < 0) + exit(128); + die_on_index_match(add_data.sm_path, force); die_on_repo_without_commits(add_data.sm_path); diff --git a/submodule.c b/submodule.c index 71ec23ad98..0b87ae6340 100644 --- a/submodule.c +++ b/submodule.c @@ -1005,6 +1005,9 @@ static int submodule_has_commits(struct repository *r, .super_oid = super_oid }; + if (validate_submodule_path(path) < 0) + exit(128); + oid_array_for_each_unique(commits, check_has_commit, &has_commit); if (has_commit.result) { @@ -1127,6 +1130,9 @@ static int push_submodule(const char *path, const struct string_list *push_options, int dry_run) { + if (validate_submodule_path(path) < 0) + exit(128); + if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) { struct child_process cp = CHILD_PROCESS_INIT; strvec_push(&cp.args, "push"); @@ -1176,6 +1182,9 @@ static void submodule_push_check(const char *path, const char *head, struct child_process cp = CHILD_PROCESS_INIT; int i; + if (validate_submodule_path(path) < 0) + exit(128); + strvec_push(&cp.args, "submodule--helper"); strvec_push(&cp.args, "push-check"); strvec_push(&cp.args, head); @@ -1507,6 +1516,9 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf struct fetch_task *task = xmalloc(sizeof(*task)); memset(task, 0, sizeof(*task)); + if (validate_submodule_path(path) < 0) + exit(128); + task->sub = submodule_from_path(spf->r, treeish_name, path); if (!task->sub) { @@ -1879,6 +1891,9 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked) const char *git_dir; int ignore_cp_exit_code = 0; + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&buf, "%s/.git", path); git_dir = read_gitfile(buf.buf); if (!git_dir) @@ -1955,6 +1970,9 @@ int submodule_uses_gitfile(const char *path) struct strbuf buf = STRBUF_INIT; const char *git_dir; + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&buf, "%s/.git", path); git_dir = read_gitfile(buf.buf); if (!git_dir) { @@ -1994,6 +2012,9 @@ int bad_to_remove_submodule(const char *path, unsigned flags) struct strbuf buf = STRBUF_INIT; int ret = 0; + if (validate_submodule_path(path) < 0) + exit(128); + if (!file_exists(path) || is_empty_dir(path)) return 0; @@ -2044,6 +2065,9 @@ void submodule_unset_core_worktree(const struct submodule *sub) { struct strbuf config_path = STRBUF_INIT; + if (validate_submodule_path(sub->path) < 0) + exit(128); + submodule_name_to_gitdir(&config_path, the_repository, sub->name); strbuf_addstr(&config_path, "/config"); @@ -2066,6 +2090,9 @@ static int submodule_has_dirty_index(const struct submodule *sub) { struct child_process cp = CHILD_PROCESS_INIT; + if (validate_submodule_path(sub->path) < 0) + exit(128); + prepare_submodule_repo_env(&cp.env); cp.git_cmd = 1; @@ -2083,6 +2110,10 @@ static int submodule_has_dirty_index(const struct submodule *sub) static void submodule_reset_index(const char *path) { struct child_process cp = CHILD_PROCESS_INIT; + + if (validate_submodule_path(path) < 0) + exit(128); + prepare_submodule_repo_env(&cp.env); cp.git_cmd = 1; @@ -2287,6 +2318,34 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name) return 0; } +int validate_submodule_path(const char *path) +{ + char *p = xstrdup(path); + struct stat st; + int i, ret = 0; + char sep; + + for (i = 0; !ret && p[i]; i++) { + if (!is_dir_sep(p[i])) + continue; + + sep = p[i]; + p[i] = '\0'; + /* allow missing components, but no symlinks */ + ret = lstat(p, &st) || !S_ISLNK(st.st_mode) ? 0 : -1; + p[i] = sep; + if (ret) + error(_("expected '%.*s' in submodule path '%s' not to " + "be a symbolic link"), i, p, p); + } + if (!lstat(p, &st) && S_ISLNK(st.st_mode)) + ret = error(_("expected submodule path '%s' not to be a " + "symbolic link"), p); + free(p); + return ret; +} + + /* * Embeds a single submodules git directory into the superprojects git dir, * non recursively. @@ -2297,6 +2356,9 @@ static void relocate_single_git_dir_into_superproject(const char *path) struct strbuf new_gitdir = STRBUF_INIT; const struct submodule *sub; + if (validate_submodule_path(path) < 0) + exit(128); + if (submodule_uses_worktrees(path)) die(_("relocate_gitdir for submodule '%s' with " "more than one worktree not supported"), path); @@ -2337,6 +2399,9 @@ static void absorb_git_dir_into_superproject_recurse(const char *path) struct child_process cp = CHILD_PROCESS_INIT; + if (validate_submodule_path(path) < 0) + exit(128); + cp.dir = path; cp.git_cmd = 1; cp.no_stdin = 1; @@ -2359,6 +2424,10 @@ void absorb_git_dir_into_superproject(const char *path) int err_code; const char *sub_git_dir; struct strbuf gitdir = STRBUF_INIT; + + if (validate_submodule_path(path) < 0) + exit(128); + strbuf_addf(&gitdir, "%s/.git", path); sub_git_dir = resolve_gitdir_gently(gitdir.buf, &err_code); @@ -2501,6 +2570,9 @@ int submodule_to_gitdir(struct strbuf *buf, const char *submodule) const char *git_dir; int ret = 0; + if (validate_submodule_path(submodule) < 0) + exit(128); + strbuf_reset(buf); strbuf_addstr(buf, submodule); strbuf_complete(buf, '/'); diff --git a/submodule.h b/submodule.h index b52a4ff1e7..fb770f1687 100644 --- a/submodule.h +++ b/submodule.h @@ -148,6 +148,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r, */ int validate_submodule_git_dir(char *git_dir, const char *submodule_name); +/* + * Make sure that the given submodule path does not follow symlinks. + */ +int validate_submodule_path(const char *path); + #define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0) #define SUBMODULE_MOVE_HEAD_FORCE (1<<1) int submodule_move_head(const char *path, diff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh index a72f3cbcab..3d3c7af3ce 100755 --- a/t/t7423-submodule-symlinks.sh +++ b/t/t7423-submodule-symlinks.sh @@ -14,15 +14,16 @@ test_expect_success 'prepare' ' git commit -m submodule ' -test_expect_failure SYMLINKS 'git submodule update must not create submodule behind symlink' ' +test_expect_success SYMLINKS 'git submodule update must not create submodule behind symlink' ' rm -rf a b && mkdir b && ln -s b a && + test_path_is_missing b/sm && test_must_fail git submodule update && test_path_is_missing b/sm ' -test_expect_failure SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' ' +test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' ' rm -rf a b && mkdir b && ln -s b A && @@ -46,7 +47,7 @@ test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confu test_path_is_missing a/target/submodule_file ' -test_expect_failure SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' ' +test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' ' prepare_symlink_to_repo && rm -rf .git/modules && test_must_fail git restore --recurse-submodules a/sm && @@ -55,7 +56,7 @@ test_expect_failure SYMLINKS 'git restore --recurse-submodules must not migrate test_path_is_missing a/target/submodule_file ' -test_expect_failure SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' ' +test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' ' prepare_symlink_to_repo && rm -rf .git/modules && test_must_fail git checkout -f --recurse-submodules initial && -- cgit v1.2.3 From df93e407f0618e4a8265ac619dc7f4c7005155bc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 29 Mar 2024 11:45:01 +0100 Subject: init: refactor the template directory discovery into its own function We will need to call this function from `hook.c` to be able to prevent hooks from running that were written as part of a `clone` but did not originate from the template directory. Signed-off-by: Johannes Schindelin --- builtin/init-db.c | 22 ++++------------------ cache.h | 1 + setup.c | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 18 deletions(-) (limited to 'builtin') diff --git a/builtin/init-db.c b/builtin/init-db.c index dcaaf102ea..a101e7f94c 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -11,10 +11,6 @@ #include "parse-options.h" #include "worktree.h" -#ifndef DEFAULT_GIT_TEMPLATE_DIR -#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates" -#endif - #ifdef NO_TRUSTABLE_FILEMODE #define TEST_FILEMODE 0 #else @@ -93,8 +89,9 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path, } } -static void copy_templates(const char *template_dir, const char *init_template_dir) +static void copy_templates(const char *option_template) { + const char *template_dir = get_template_dir(option_template); struct strbuf path = STRBUF_INIT; struct strbuf template_path = STRBUF_INIT; size_t template_len; @@ -103,16 +100,8 @@ static void copy_templates(const char *template_dir, const char *init_template_d DIR *dir; char *to_free = NULL; - if (!template_dir) - template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); - if (!template_dir) - template_dir = init_template_dir; - if (!template_dir) - template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR); - if (!template_dir[0]) { - free(to_free); + if (!template_dir || !*template_dir) return; - } strbuf_addstr(&template_path, template_dir); strbuf_complete(&template_path, '/'); @@ -200,7 +189,6 @@ static int create_default_files(const char *template_path, int reinit; int filemode; struct strbuf err = STRBUF_INIT; - const char *init_template_dir = NULL; const char *work_tree = get_git_work_tree(); /* @@ -212,9 +200,7 @@ static int create_default_files(const char *template_path, * values (since we've just potentially changed what's available on * disk). */ - git_config_get_pathname("init.templatedir", &init_template_dir); - copy_templates(template_path, init_template_dir); - free((char *)init_template_dir); + copy_templates(template_path); git_config_clear(); reset_shared_repository(); git_config(git_default_config, NULL); diff --git a/cache.h b/cache.h index a46a3e4b6b..8c5fb1e1ba 100644 --- a/cache.h +++ b/cache.h @@ -656,6 +656,7 @@ int path_inside_repo(const char *prefix, const char *path); #define INIT_DB_QUIET 0x0001 #define INIT_DB_EXIST_OK 0x0002 +const char *get_template_dir(const char *option_template); int init_db(const char *git_dir, const char *real_git_dir, const char *template_dir, int hash_algo, const char *initial_branch, unsigned int flags); diff --git a/setup.c b/setup.c index 9d401ae4c8..e6e749ec4b 100644 --- a/setup.c +++ b/setup.c @@ -6,6 +6,7 @@ #include "chdir-notify.h" #include "promisor-remote.h" #include "quote.h" +#include "exec-cmd.h" static int inside_git_dir = -1; static int inside_work_tree = -1; @@ -1720,3 +1721,34 @@ int daemonize(void) return 0; #endif } + +#ifndef DEFAULT_GIT_TEMPLATE_DIR +#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates" +#endif + +const char *get_template_dir(const char *option_template) +{ + const char *template_dir = option_template; + + if (!template_dir) + template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); + if (!template_dir) { + static const char *init_template_dir; + static int initialized; + + if (!initialized) { + git_config_get_pathname("init.templatedir", + &init_template_dir); + initialized = 1; + } + template_dir = init_template_dir; + } + if (!template_dir) { + static char *dir; + + if (!dir) + dir = system_path(DEFAULT_GIT_TEMPLATE_DIR); + template_dir = dir; + } + return template_dir; +} -- cgit v1.2.3 From 8db1e8743c0f1ed241f6a1b8bf55b6fef07d6751 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 28 Mar 2024 19:21:06 +0100 Subject: 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 `/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 --- builtin/clone.c | 12 +++++++++++- hook.c | 32 ++++++++++++++++++++++++++++++++ t/t5601-clone.sh | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) (limited to 'builtin') diff --git a/builtin/clone.c b/builtin/clone.c index 3c2ae31a55..35a73ed0a7 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -908,6 +908,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix) int err = 0, complete_refs_before_fetch = 1; int submodule_progress; int filter_submodules = 0; + const char *template_dir; + char *template_dir_dup = NULL; struct transport_ls_refs_options transport_ls_refs_options = TRANSPORT_LS_REFS_OPTIONS_INIT; @@ -927,6 +929,13 @@ int cmd_clone(int argc, const char **argv, const char *prefix) usage_msg_opt(_("You must specify a repository to clone."), builtin_clone_usage, builtin_clone_options); + xsetenv("GIT_CLONE_PROTECTION_ACTIVE", "true", 0 /* allow user override */); + template_dir = get_template_dir(option_template); + if (*template_dir && !is_absolute_path(template_dir)) + template_dir = template_dir_dup = + absolute_pathdup(template_dir); + xsetenv("GIT_CLONE_TEMPLATE_DIR", template_dir, 1); + if (option_depth || option_since || option_not.nr) deepen = 1; if (option_single_branch == -1) @@ -1074,7 +1083,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) } } - init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL, + init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL, INIT_DB_QUIET); if (real_git_dir) { @@ -1392,6 +1401,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix) free(unborn_head); free(dir); free(path); + free(template_dir_dup); UNLEAK(repo); junk_mode = JUNK_LEAVE_ALL; diff --git a/hook.c b/hook.c index 22b274b60b..632b537b99 100644 --- a/hook.c +++ b/hook.c @@ -3,6 +3,30 @@ #include "run-command.h" #include "config.h" +static int identical_to_template_hook(const char *name, const char *path) +{ + const char *env = getenv("GIT_CLONE_TEMPLATE_DIR"); + const char *template_dir = get_template_dir(env && *env ? env : NULL); + struct strbuf template_path = STRBUF_INIT; + int found_template_hook, ret; + + strbuf_addf(&template_path, "%s/hooks/%s", template_dir, name); + found_template_hook = access(template_path.buf, X_OK) >= 0; +#ifdef STRIP_EXTENSION + if (!found_template_hook) { + strbuf_addstr(&template_path, STRIP_EXTENSION); + found_template_hook = access(template_path.buf, X_OK) >= 0; + } +#endif + if (!found_template_hook) + return 0; + + ret = do_files_match(template_path.buf, path); + + strbuf_release(&template_path); + return ret; +} + const char *find_hook(const char *name) { static struct strbuf path = STRBUF_INIT; @@ -38,6 +62,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)) + 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); return path.buf; } diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index fd02984330..20deca0231 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -771,6 +771,57 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe git clone --filter=blob:limit=0 "file://$(pwd)/server" client ' +test_expect_success 'clone with init.templatedir runs hooks' ' + git init tmpl/hooks && + write_script tmpl/hooks/post-checkout <<-EOF && + echo HOOK-RUN >&2 + echo I was here >hook.run + EOF + git -C tmpl/hooks add . && + test_tick && + git -C tmpl/hooks commit -m post-checkout && + + test_when_finished "git config --global --unset init.templateDir || :" && + test_when_finished "git config --unset init.templateDir || :" && + ( + sane_unset GIT_TEMPLATE_DIR && + NO_SET_GIT_TEMPLATE_DIR=t && + export NO_SET_GIT_TEMPLATE_DIR && + + git -c core.hooksPath="$(pwd)/tmpl/hooks" \ + clone tmpl/hooks hook-run-hookspath 2>err && + ! grep "active .* hook found" err && + test_path_is_file hook-run-hookspath/hook.run && + + git -c init.templateDir="$(pwd)/tmpl" \ + clone tmpl/hooks hook-run-config 2>err && + ! grep "active .* hook found" err && + test_path_is_file hook-run-config/hook.run && + + git clone --template=tmpl tmpl/hooks hook-run-option 2>err && + ! grep "active .* hook found" err && + test_path_is_file hook-run-option/hook.run && + + git config --global init.templateDir "$(pwd)/tmpl" && + git clone tmpl/hooks hook-run-global-config 2>err && + git config --global --unset init.templateDir && + ! grep "active .* hook found" err && + test_path_is_file hook-run-global-config/hook.run && + + # clone ignores local `init.templateDir`; need to create + # a new repository because we deleted `.git/` in the + # `setup` test case above + git init local-clone && + cd local-clone && + + git config init.templateDir "$(pwd)/../tmpl" && + git clone ../tmpl/hooks hook-run-local-config 2>err && + git config --unset init.templateDir && + ! grep "active .* hook found" err && + test_path_is_missing hook-run-local-config/hook.run + ) +' + . "$TEST_DIRECTORY"/lib-httpd.sh start_httpd -- cgit v1.2.3