setup.c: create `safe.bareRepository`

There is a known social engineering attack that takes advantage of the fact that a working tree can include an entire bare repository, including a config file. A user could run a Git command inside the bare repository thinking that the config file of the 'outer' repository would be used, but in reality, the bare repository's config file (which is attacker-controlled) is used, which may result in arbitrary code execution. See [1] for a fuller description and deeper discussion. A simple mitigation is to forbid bare repositories unless specified via `--git-dir` or `GIT_DIR`. In environments that don't use bare repositories, this would be minimally disruptive. Create a config variable, `safe.bareRepository`, that tells Git whether or not to die() when working with a bare repository. This config is an enum of: - "all": allow all bare repositories (this is the default) - "explicit": only allow bare repositories specified via --git-dir or GIT_DIR. If we want to protect users from such attacks by default, neither value will suffice - "all" provides no protection, but "explicit" is impractical for bare repository users. A more usable default would be to allow only non-embedded bare repositories ([2] contains one such proposal), but detecting if a repository is embedded is potentially non-trivial, so this work is not implemented in this series. [1]: https://lore.kernel.org/git/kl6lsfqpygsj.fsf@chooglen-macbookpro.roam.corp.google.com [2]: https://lore.kernel.org/git/5b969c5e-e802-c447-ad25-6acc0b784582@github.com Signed-off-by: Glen Choo <chooglen@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Glen Choo committed Jul 14, 2022 at 21:28 UTC 8d1a7448206e11cdea657c35b04cc49db39be933
3 files changed +129 -1
Documentation/config/safe.txt
+19
@@ -1,3 +1,22 @@
1 +safe.bareRepository::
2 + Specifies which bare repositories Git will work with. The currently
3 + supported values are:
4 ++
5 +* `all`: Git works with all bare repositories. This is the default.
6 +* `explicit`: Git only works with bare repositories specified via
7 + the top-level `--git-dir` command-line option, or the `GIT_DIR`
8 + environment variable (see linkgit:git[1]).
9 ++
10 +If you do not use bare repositories in your workflow, then it may be
11 +beneficial to set `safe.bareRepository` to `explicit` in your global
12 +config. This will protect you from attacks that involve cloning a
13 +repository that contains a bare repository and running a Git command
14 +within that directory.
15 ++
16 +This config setting is only respected in protected configuration (see
17 +<<SCOPES>>). This prevents the untrusted repository from tampering with
18 +this value.
19 +
20 safe.directory::
21 These config entries specify Git-tracked directories that are
22 considered safe even if they are owned by someone other than the
setup.c
+56 -1
@@ -10,6 +10,10 @@
10 static int inside_git_dir = -1;
11 static int inside_work_tree = -1;
12 static int work_tree_config_is_bogus;
13 +enum allowed_bare_repo {
14 + ALLOWED_BARE_REPO_EXPLICIT = 0,
15 + ALLOWED_BARE_REPO_ALL,
16 +};
17
18 static struct startup_info the_startup_info;
19 struct startup_info *startup_info = &the_startup_info;
@@ -1160,6 +1164,46 @@ static int ensure_valid_ownership(const char *gitfile,
1164 return data.is_safe;
1165 }
1166
1167 +static int allowed_bare_repo_cb(const char *key, const char *value, void *d)
1168 +{
1169 + enum allowed_bare_repo *allowed_bare_repo = d;
1170 +
1171 + if (strcasecmp(key, "safe.bareRepository"))
1172 + return 0;
1173 +
1174 + if (!strcmp(value, "explicit")) {
1175 + *allowed_bare_repo = ALLOWED_BARE_REPO_EXPLICIT;
1176 + return 0;
1177 + }
1178 + if (!strcmp(value, "all")) {
1179 + *allowed_bare_repo = ALLOWED_BARE_REPO_ALL;
1180 + return 0;
1181 + }
1182 + return -1;
1183 +}
1184 +
1185 +static enum allowed_bare_repo get_allowed_bare_repo(void)
1186 +{
1187 + enum allowed_bare_repo result = ALLOWED_BARE_REPO_ALL;
1188 + git_protected_config(allowed_bare_repo_cb, &result);
1189 + return result;
1190 +}
1191 +
1192 +static const char *allowed_bare_repo_to_string(
1193 + enum allowed_bare_repo allowed_bare_repo)
1194 +{
1195 + switch (allowed_bare_repo) {
1196 + case ALLOWED_BARE_REPO_EXPLICIT:
1197 + return "explicit";
1198 + case ALLOWED_BARE_REPO_ALL:
1199 + return "all";
1200 + default:
1201 + BUG("invalid allowed_bare_repo %d",
1202 + allowed_bare_repo);
1203 + }
1204 + return NULL;
1205 +}
1206 +
1207 enum discovery_result {
1208 GIT_DIR_NONE = 0,
1209 GIT_DIR_EXPLICIT,
@@ -1169,7 +1213,8 @@ enum discovery_result {
1213 GIT_DIR_HIT_CEILING = -1,
1214 GIT_DIR_HIT_MOUNT_POINT = -2,
1215 GIT_DIR_INVALID_GITFILE = -3,
1172 - GIT_DIR_INVALID_OWNERSHIP = -4
1216 + GIT_DIR_INVALID_OWNERSHIP = -4,
1217 + GIT_DIR_DISALLOWED_BARE = -5,
1218 };
1219
1220 /*
@@ -1297,6 +1342,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
1342 }
1343
1344 if (is_git_directory(dir->buf)) {
1345 + if (get_allowed_bare_repo() == ALLOWED_BARE_REPO_EXPLICIT)
1346 + return GIT_DIR_DISALLOWED_BARE;
1347 if (!ensure_valid_ownership(NULL, NULL, dir->buf))
1348 return GIT_DIR_INVALID_OWNERSHIP;
1349 strbuf_addstr(gitdir, ".");
@@ -1443,6 +1490,14 @@ const char *setup_git_directory_gently(int *nongit_ok)
1490 }
1491 *nongit_ok = 1;
1492 break;
1493 + case GIT_DIR_DISALLOWED_BARE:
1494 + if (!nongit_ok) {
1495 + die(_("cannot use bare repository '%s' (safe.bareRepository is '%s')"),
1496 + dir.buf,
1497 + allowed_bare_repo_to_string(get_allowed_bare_repo()));
1498 + }
1499 + *nongit_ok = 1;
1500 + break;
1501 case GIT_DIR_NONE:
1502 /*
1503 * As a safeguard against setup_git_directory_gently_1 returning
t/t0035-safe-bare-repository.sh new
+54
@@ -0,0 +1,54 @@
1 +#!/bin/sh
2 +
3 +test_description='verify safe.bareRepository checks'
4 +
5 +TEST_PASSES_SANITIZE_LEAK=true
6 +. ./test-lib.sh
7 +
8 +pwd="$(pwd)"
9 +
10 +expect_accepted () {
11 + git "$@" rev-parse --git-dir
12 +}
13 +
14 +expect_rejected () {
15 + test_must_fail git "$@" rev-parse --git-dir 2>err &&
16 + grep -F "cannot use bare repository" err
17 +}
18 +
19 +test_expect_success 'setup bare repo in worktree' '
20 + git init outer-repo &&
21 + git init --bare outer-repo/bare-repo
22 +'
23 +
24 +test_expect_success 'safe.bareRepository unset' '
25 + expect_accepted -C outer-repo/bare-repo
26 +'
27 +
28 +test_expect_success 'safe.bareRepository=all' '
29 + test_config_global safe.bareRepository all &&
30 + expect_accepted -C outer-repo/bare-repo
31 +'
32 +
33 +test_expect_success 'safe.bareRepository=explicit' '
34 + test_config_global safe.bareRepository explicit &&
35 + expect_rejected -C outer-repo/bare-repo
36 +'
37 +
38 +test_expect_success 'safe.bareRepository in the repository' '
39 + # safe.bareRepository must not be "explicit", otherwise
40 + # git config fails with "fatal: not in a git directory" (like
41 + # safe.directory)
42 + test_config -C outer-repo/bare-repo safe.bareRepository \
43 + all &&
44 + test_config_global safe.bareRepository explicit &&
45 + expect_rejected -C outer-repo/bare-repo
46 +'
47 +
48 +test_expect_success 'safe.bareRepository on the command line' '
49 + test_config_global safe.bareRepository explicit &&
50 + expect_accepted -C outer-repo/bare-repo \
51 + -c safe.bareRepository=all
52 +'
53 +
54 +test_done