Skip to content

Commit 0060fd1

Browse files
committed
clone --recurse-submodules: prevent name squatting on Windows
In addition to preventing `.git` from being tracked by Git, on Windows we also have to prevent `git~1` from being tracked, as the default NTFS short name (also known as the "8.3 filename") for the file name `.git` is `git~1`, otherwise it would be possible for malicious repositories to write directly into the `.git/` directory, e.g. a `post-checkout` hook that would then be executed _during_ a recursive clone. When we implemented appropriate protections in 2b4c6ef (read-cache: optionally disallow NTFS .git variants, 2014-12-16), we had analyzed carefully that the `.git` directory or file would be guaranteed to be the first directory entry to be written. Otherwise it would be possible e.g. for a file named `..git` to be assigned the short name `git~1` and subsequently, the short name generated for `.git` would be `git~2`. Or `git~3`. Or even `~9999999` (for a detailed explanation of the lengths we have to go to protect `.gitmodules`, see the commit message of e7cb0b4 (is_ntfs_dotgit: match other .git files, 2018-05-11)). However, by exploiting two issues (that will be addressed in a related patch series close by), it is currently possible to clone a submodule into a non-empty directory: - On Windows, file names cannot end in a space or a period (for historical reasons: the period separating the base name from the file extension was not actually written to disk, and the base name/file extension was space-padded to the full 8/3 characters, respectively). Helpfully, when creating a directory under the name, say, `sub.`, that trailing period is trimmed automatically and the actual name on disk is `sub`. This means that while Git thinks that the submodule names `sub` and `sub.` are different, they both access `.git/modules/sub/`. - While the backslash character is a valid file name character on Linux, it is not so on Windows. As Git tries to be cross-platform, it therefore allows backslash characters in the file names stored in tree objects. Which means that it is totally possible that a submodule `c` sits next to a file `c\..git`, and on Windows, during recursive clone a file called `..git` will be written into `c/`, of course _before_ the submodule is cloned. Note that the actual exploit is not quite as simple as having a submodule `c` next to a file `c\..git`, as we have to make sure that the directory `.git/modules/b` already exists when the submodule is checked out, otherwise a different code path is taken in `module_clone()` that does _not_ allow a non-empty submodule directory to exist already. Even if we will address both issues nearby (the next commit will disallow backslash characters in tree entries' file names on Windows, and another patch will disallow creating directories/files with trailing spaces or periods), it is a wise idea to defend in depth against this sort of attack vector: when submodules are cloned recursively, we now _require_ the directory to be empty, addressing CVE-2019-1349. Note: the code path we patch is shared with the code path of `git submodule update --init`, which must not expect, in general, that the directory is empty. Hence we have to introduce the new option `--force-init` and hand it all the way down from `git submodule` to the actual `git submodule--helper` process that performs the initial clone. Reported-by: Nicolas Joly <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
1 parent a52ed76 commit 0060fd1

File tree

4 files changed

+50
-2
lines changed

4 files changed

+50
-2
lines changed

builtin/clone.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ static int checkout(int submodule_progress)
757757

758758
if (!err && (option_recurse_submodules.nr > 0)) {
759759
struct argv_array args = ARGV_ARRAY_INIT;
760-
argv_array_pushl(&args, "submodule", "update", "--init", "--recursive", NULL);
760+
argv_array_pushl(&args, "submodule", "update", "--require-init", "--recursive", NULL);
761761

762762
if (option_shallow_submodules == 1)
763763
argv_array_push(&args, "--depth=1");

builtin/submodule--helper.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "remote.h"
1414
#include "refs.h"
1515
#include "connect.h"
16+
#include "dir.h"
1617

1718
static char *get_default_remote(void)
1819
{
@@ -623,6 +624,7 @@ static int module_clone(int argc, const char **argv, const char *prefix)
623624
char *p, *path = NULL, *sm_gitdir;
624625
struct strbuf sb = STRBUF_INIT;
625626
struct string_list reference = STRING_LIST_INIT_NODUP;
627+
int require_init = 0;
626628
char *sm_alternate = NULL, *error_strategy = NULL;
627629

628630
struct option module_clone_options[] = {
@@ -647,6 +649,8 @@ static int module_clone(int argc, const char **argv, const char *prefix)
647649
OPT__QUIET(&quiet, "Suppress output for cloning a submodule"),
648650
OPT_BOOL(0, "progress", &progress,
649651
N_("force cloning progress")),
652+
OPT_BOOL(0, "require-init", &require_init,
653+
N_("disallow cloning into non-empty directory")),
650654
OPT_END()
651655
};
652656

@@ -685,6 +689,8 @@ static int module_clone(int argc, const char **argv, const char *prefix)
685689
die(_("clone of '%s' into submodule path '%s' failed"),
686690
url, path);
687691
} else {
692+
if (require_init && !access(path, X_OK) && !is_empty_dir(path))
693+
die(_("directory not empty: '%s'"), path);
688694
if (safe_create_leading_directories_const(path) < 0)
689695
die(_("could not create directory '%s'"), path);
690696
strbuf_addf(&sb, "%s/index", sm_gitdir);
@@ -733,6 +739,7 @@ struct submodule_update_clone {
733739
int quiet;
734740
int recommend_shallow;
735741
struct string_list references;
742+
unsigned require_init;
736743
const char *depth;
737744
const char *recursive_prefix;
738745
const char *prefix;
@@ -748,7 +755,7 @@ struct submodule_update_clone {
748755
int failed_clones_nr, failed_clones_alloc;
749756
};
750757
#define SUBMODULE_UPDATE_CLONE_INIT {0, MODULE_LIST_INIT, 0, \
751-
SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, \
758+
SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, \
752759
NULL, NULL, NULL, \
753760
STRING_LIST_INIT_DUP, 0, NULL, 0, 0}
754761

@@ -850,6 +857,8 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,
850857
argv_array_pushl(&child->args, "--prefix", suc->prefix, NULL);
851858
if (suc->recommend_shallow && sub->recommend_shallow == 1)
852859
argv_array_push(&child->args, "--depth=1");
860+
if (suc->require_init)
861+
argv_array_push(&child->args, "--require-init");
853862
argv_array_pushl(&child->args, "--path", sub->path, NULL);
854863
argv_array_pushl(&child->args, "--name", sub->name, NULL);
855864
argv_array_pushl(&child->args, "--url", sub->url, NULL);
@@ -992,6 +1001,8 @@ static int update_clone(int argc, const char **argv, const char *prefix)
9921001
OPT__QUIET(&suc.quiet, N_("don't print cloning progress")),
9931002
OPT_BOOL(0, "progress", &suc.progress,
9941003
N_("force cloning progress")),
1004+
OPT_BOOL(0, "require-init", &suc.require_init,
1005+
N_("disallow cloning into non-empty directory")),
9951006
OPT_END()
9961007
};
9971008

git-submodule.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ reference=
3434
cached=
3535
recursive=
3636
init=
37+
require_init=
3738
files=
3839
remote=
3940
nofetch=
@@ -528,6 +529,10 @@ cmd_update()
528529
-i|--init)
529530
init=1
530531
;;
532+
--require-init)
533+
init=1
534+
require_init=1
535+
;;
531536
--remote)
532537
remote=1
533538
;;
@@ -606,6 +611,7 @@ cmd_update()
606611
${update:+--update "$update"} \
607612
${reference:+"$reference"} \
608613
${depth:+--depth "$depth"} \
614+
${require_init:+--require-init} \
609615
${recommend_shallow:+"$recommend_shallow"} \
610616
${jobs:+$jobs} \
611617
"$@" || echo "#unmatched" $?

t/t7415-submodule-names.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,35 @@ test_expect_success 'clone evil superproject' '
7373
! grep "RUNNING POST CHECKOUT" output
7474
'
7575

76+
test_expect_success MINGW 'prevent git~1 squatting on Windows' '
77+
git init squatting &&
78+
(
79+
cd squatting &&
80+
mkdir a &&
81+
touch a/..git &&
82+
git add a/..git &&
83+
test_tick &&
84+
git commit -m initial &&
85+
86+
modules="$(test_write_lines \
87+
"[submodule \"b.\"]" "url = ." "path = c" \
88+
"[submodule \"b\"]" "url = ." "path = d\\\\a" |
89+
git hash-object -w --stdin)" &&
90+
rev="$(git rev-parse --verify HEAD)" &&
91+
hash="$(echo x | git hash-object -w --stdin)" &&
92+
git update-index --add \
93+
--cacheinfo 100644,$modules,.gitmodules \
94+
--cacheinfo 160000,$rev,c \
95+
--cacheinfo 160000,$rev,d\\a \
96+
--cacheinfo 100644,$hash,d./a/x \
97+
--cacheinfo 100644,$hash,d./a/..git &&
98+
test_tick &&
99+
git commit -m "module"
100+
) &&
101+
test_must_fail git \
102+
clone --recurse-submodules squatting squatting-clone 2>err &&
103+
test_i18ngrep "directory not empty" err &&
104+
! grep gitdir squatting-clone/d/a/git~2
105+
'
106+
76107
test_done

0 commit comments

Comments
 (0)