Skip to content

Commit 86cb6a3

Browse files
committed
Merge branch 'icasefs-symlink-confusion'
This topic branch fixes two vulnerabilities: - Recursive clones on case-insensitive filesystems that support symbolic links are susceptible to case confusion that can be exploited to execute just-cloned code during the clone operation. - Repositories can be configured to execute arbitrary code during local clones. To address this, the ownership checks introduced in v2.30.3 are now extended to cover cloning local repositories. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 9e06401 + e8d0608 commit 86cb6a3

16 files changed

+559
-57
lines changed

Documentation/git-upload-pack.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,37 @@ ENVIRONMENT
5555
admins may need to configure some transports to allow this
5656
variable to be passed. See the discussion in linkgit:git[1].
5757

58+
`GIT_NO_LAZY_FETCH`::
59+
When cloning or fetching from a partial repository (i.e., one
60+
itself cloned with `--filter`), the server-side `upload-pack`
61+
may need to fetch extra objects from its upstream in order to
62+
complete the request. By default, `upload-pack` will refuse to
63+
perform such a lazy fetch, because `git fetch` may run arbitrary
64+
commands specified in configuration and hooks of the source
65+
repository (and `upload-pack` tries to be safe to run even in
66+
untrusted `.git` directories).
67+
+
68+
This is implemented by having `upload-pack` internally set the
69+
`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it
70+
(because you are fetching from a partial clone, and you are sure
71+
you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to
72+
`0`.
73+
74+
SECURITY
75+
--------
76+
77+
Most Git commands should not be run in an untrusted `.git` directory
78+
(see the section `SECURITY` in linkgit:git[1]). `upload-pack` tries to
79+
avoid any dangerous configuration options or hooks from the repository
80+
it's serving, making it safe to clone an untrusted directory and run
81+
commands on the resulting clone.
82+
83+
For an extra level of safety, you may be able to run `upload-pack` as an
84+
alternate user. The details will be platform dependent, but on many
85+
systems you can run:
86+
87+
git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' ...
88+
5889
SEE ALSO
5990
--------
6091
linkgit:gitnamespaces[7]

Documentation/git.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,37 @@ The index is also capable of storing multiple entries (called "stages")
10321032
for a given pathname. These stages are used to hold the various
10331033
unmerged version of a file when a merge is in progress.
10341034

1035+
SECURITY
1036+
--------
1037+
1038+
Some configuration options and hook files may cause Git to run arbitrary
1039+
shell commands. Because configuration and hooks are not copied using
1040+
`git clone`, it is generally safe to clone remote repositories with
1041+
untrusted content, inspect them with `git log`, and so on.
1042+
1043+
However, it is not safe to run Git commands in a `.git` directory (or
1044+
the working tree that surrounds it) when that `.git` directory itself
1045+
comes from an untrusted source. The commands in its config and hooks
1046+
are executed in the usual way.
1047+
1048+
By default, Git will refuse to run when the repository is owned by
1049+
someone other than the user running the command. See the entry for
1050+
`safe.directory` in linkgit:git-config[1]. While this can help protect
1051+
you in a multi-user environment, note that you can also acquire
1052+
untrusted repositories that are owned by you (for example, if you
1053+
extract a zip file or tarball from an untrusted source). In such cases,
1054+
you'd need to "sanitize" the untrusted repository first.
1055+
1056+
If you have an untrusted `.git` directory, you should first clone it
1057+
with `git clone --no-local` to obtain a clean copy. Git does restrict
1058+
the set of options and hooks that will be run by `upload-pack`, which
1059+
handles the server side of a clone or fetch, but beware that the
1060+
surface area for attack against `upload-pack` is large, so this does
1061+
carry some risk. The safest thing is to serve the repository as an
1062+
unprivileged user (either via linkgit:git-daemon[1], ssh, or using
1063+
other tools to change user ids). See the discussion in the `SECURITY`
1064+
section of linkgit:git-upload-pack[1].
1065+
10351066
FURTHER DOCUMENTATION
10361067
---------------------
10371068

builtin/submodule--helper.c

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
294294
struct child_process cp = CHILD_PROCESS_INIT;
295295
char *displaypath;
296296

297+
if (validate_submodule_path(path) < 0)
298+
exit(128);
299+
297300
displaypath = get_submodule_displaypath(path, info->prefix);
298301

299302
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,
620623
.free_removed_argv_elements = 1,
621624
};
622625

626+
if (validate_submodule_path(path) < 0)
627+
exit(128);
628+
623629
if (!submodule_from_path(the_repository, null_oid(), path))
624630
die(_("no submodule mapping found in .gitmodules for path '%s'"),
625631
path);
@@ -1220,6 +1226,9 @@ static void sync_submodule(const char *path, const char *prefix,
12201226
if (!is_submodule_active(the_repository, path))
12211227
return;
12221228

1229+
if (validate_submodule_path(path) < 0)
1230+
exit(128);
1231+
12231232
sub = submodule_from_path(the_repository, null_oid(), path);
12241233

12251234
if (sub && sub->url) {
@@ -1360,6 +1369,9 @@ static void deinit_submodule(const char *path, const char *prefix,
13601369
struct strbuf sb_config = STRBUF_INIT;
13611370
char *sub_git_dir = xstrfmt("%s/.git", path);
13621371

1372+
if (validate_submodule_path(path) < 0)
1373+
exit(128);
1374+
13631375
sub = submodule_from_path(the_repository, null_oid(), path);
13641376

13651377
if (!sub || !sub->name)
@@ -1641,16 +1653,42 @@ static char *clone_submodule_sm_gitdir(const char *name)
16411653
return sm_gitdir;
16421654
}
16431655

1656+
static int dir_contains_only_dotgit(const char *path)
1657+
{
1658+
DIR *dir = opendir(path);
1659+
struct dirent *e;
1660+
int ret = 1;
1661+
1662+
if (!dir)
1663+
return 0;
1664+
1665+
e = readdir_skip_dot_and_dotdot(dir);
1666+
if (!e)
1667+
ret = 0;
1668+
else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
1669+
(e = readdir_skip_dot_and_dotdot(dir))) {
1670+
error("unexpected item '%s' in '%s'", e->d_name, path);
1671+
ret = 0;
1672+
}
1673+
1674+
closedir(dir);
1675+
return ret;
1676+
}
1677+
16441678
static int clone_submodule(const struct module_clone_data *clone_data,
16451679
struct string_list *reference)
16461680
{
16471681
char *p;
16481682
char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
16491683
char *sm_alternate = NULL, *error_strategy = NULL;
1684+
struct stat st;
16501685
struct child_process cp = CHILD_PROCESS_INIT;
16511686
const char *clone_data_path = clone_data->path;
16521687
char *to_free = NULL;
16531688

1689+
if (validate_submodule_path(clone_data_path) < 0)
1690+
exit(128);
1691+
16541692
if (!is_absolute_path(clone_data->path))
16551693
clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(),
16561694
clone_data->path);
@@ -1660,6 +1698,10 @@ static int clone_submodule(const struct module_clone_data *clone_data,
16601698
"git dir"), sm_gitdir);
16611699

16621700
if (!file_exists(sm_gitdir)) {
1701+
if (clone_data->require_init && !stat(clone_data_path, &st) &&
1702+
!is_empty_dir(clone_data_path))
1703+
die(_("directory not empty: '%s'"), clone_data_path);
1704+
16631705
if (safe_create_leading_directories_const(sm_gitdir) < 0)
16641706
die(_("could not create directory '%s'"), sm_gitdir);
16651707

@@ -1704,10 +1746,18 @@ static int clone_submodule(const struct module_clone_data *clone_data,
17041746
if(run_command(&cp))
17051747
die(_("clone of '%s' into submodule path '%s' failed"),
17061748
clone_data->url, clone_data_path);
1749+
1750+
if (clone_data->require_init && !stat(clone_data_path, &st) &&
1751+
!dir_contains_only_dotgit(clone_data_path)) {
1752+
char *dot_git = xstrfmt("%s/.git", clone_data_path);
1753+
unlink(dot_git);
1754+
free(dot_git);
1755+
die(_("directory not empty: '%s'"), clone_data_path);
1756+
}
17071757
} else {
17081758
char *path;
17091759

1710-
if (clone_data->require_init && !access(clone_data_path, X_OK) &&
1760+
if (clone_data->require_init && !stat(clone_data_path, &st) &&
17111761
!is_empty_dir(clone_data_path))
17121762
die(_("directory not empty: '%s'"), clone_data_path);
17131763
if (safe_create_leading_directories_const(clone_data_path) < 0)
@@ -1717,6 +1767,23 @@ static int clone_submodule(const struct module_clone_data *clone_data,
17171767
free(path);
17181768
}
17191769

1770+
/*
1771+
* We already performed this check at the beginning of this function,
1772+
* before cloning the objects. This tries to detect racy behavior e.g.
1773+
* in parallel clones, where another process could easily have made the
1774+
* gitdir nested _after_ it was created.
1775+
*
1776+
* To prevent further harm coming from this unintentionally-nested
1777+
* gitdir, let's disable it by deleting the `HEAD` file.
1778+
*/
1779+
if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {
1780+
char *head = xstrfmt("%s/HEAD", sm_gitdir);
1781+
unlink(head);
1782+
free(head);
1783+
die(_("refusing to create/use '%s' in another submodule's "
1784+
"git dir"), sm_gitdir);
1785+
}
1786+
17201787
connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);
17211788

17221789
p = git_pathdup_submodule(clone_data_path, "config");
@@ -2490,6 +2557,9 @@ static int update_submodule(struct update_data *update_data)
24902557
{
24912558
int ret;
24922559

2560+
if (validate_submodule_path(update_data->sm_path) < 0)
2561+
return -1;
2562+
24932563
ret = determine_submodule_update_strategy(the_repository,
24942564
update_data->just_cloned,
24952565
update_data->sm_path,
@@ -2597,12 +2667,21 @@ static int update_submodules(struct update_data *update_data)
25972667

25982668
for (i = 0; i < suc.update_clone_nr; i++) {
25992669
struct update_clone_data ucd = suc.update_clone[i];
2600-
int code;
2670+
int code = 128;
26012671

26022672
oidcpy(&update_data->oid, &ucd.oid);
26032673
update_data->just_cloned = ucd.just_cloned;
26042674
update_data->sm_path = ucd.sub->path;
26052675

2676+
/*
2677+
* Verify that the submodule path does not contain any
2678+
* symlinks; if it does, it might have been tampered with.
2679+
* TODO: allow exempting it via
2680+
* `safe.submodule.path` or something
2681+
*/
2682+
if (validate_submodule_path(update_data->sm_path) < 0)
2683+
goto fail;
2684+
26062685
code = ensure_core_worktree(update_data->sm_path);
26072686
if (code)
26082687
goto fail;
@@ -3309,6 +3388,9 @@ static int module_add(int argc, const char **argv, const char *prefix)
33093388
normalize_path_copy(add_data.sm_path, add_data.sm_path);
33103389
strip_dir_trailing_slashes(add_data.sm_path);
33113390

3391+
if (validate_submodule_path(add_data.sm_path) < 0)
3392+
exit(128);
3393+
33123394
die_on_index_match(add_data.sm_path, force);
33133395
die_on_repo_without_commits(add_data.sm_path);
33143396

builtin/upload-pack.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
3535

3636
packet_trace_identity("upload-pack");
3737
read_replace_refs = 0;
38+
/* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
39+
xsetenv("GIT_NO_LAZY_FETCH", "1", 0);
3840

3941
argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);
4042

cache.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,18 @@ void set_git_work_tree(const char *tree);
606606

607607
#define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES"
608608

609+
/*
610+
* Check if a repository is safe and die if it is not, by verifying the
611+
* ownership of the worktree (if any), the git directory, and the gitfile (if
612+
* any).
613+
*
614+
* Exemptions for known-safe repositories can be added via `safe.directory`
615+
* config settings; for non-bare repositories, their worktree needs to be
616+
* added, for bare ones their git directory.
617+
*/
618+
void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
619+
const char *gitdir);
620+
609621
void setup_work_tree(void);
610622
/*
611623
* Find the commondir and gitdir of the repository that contains the current

path.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ const char *enter_repo(const char *path, int strict)
840840
if (!suffix[i])
841841
return NULL;
842842
gitfile = read_gitfile(used_path.buf);
843+
die_upon_dubious_ownership(gitfile, NULL, used_path.buf);
843844
if (gitfile) {
844845
strbuf_reset(&used_path);
845846
strbuf_addstr(&used_path, gitfile);
@@ -850,6 +851,7 @@ const char *enter_repo(const char *path, int strict)
850851
}
851852
else {
852853
const char *gitfile = read_gitfile(path);
854+
die_upon_dubious_ownership(gitfile, NULL, path);
853855
if (gitfile)
854856
path = gitfile;
855857
if (chdir(path))

promisor-remote.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ static int fetch_objects(struct repository *repo,
2020
int i;
2121
FILE *child_in;
2222

23+
/* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
24+
if (git_env_bool("GIT_NO_LAZY_FETCH", 0)) {
25+
static int warning_shown;
26+
if (!warning_shown) {
27+
warning_shown = 1;
28+
warning(_("lazy fetching disabled; some objects may not be available"));
29+
}
30+
return -1;
31+
}
32+
2333
child.git_cmd = 1;
2434
child.in = -1;
2535
if (repo != the_repository)

0 commit comments

Comments
 (0)