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>
This commit is contained in:
Glen Choo 2022-07-14 21:28:01 +00:00 committed by Junio C Hamano
parent 6061601d9f
commit 8d1a744820
3 changed files with 129 additions and 1 deletions

View File

@ -1,3 +1,22 @@
safe.bareRepository::
Specifies which bare repositories Git will work with. The currently
supported values are:
+
* `all`: Git works with all bare repositories. This is the default.
* `explicit`: Git only works with bare repositories specified via
the top-level `--git-dir` command-line option, or the `GIT_DIR`
environment variable (see linkgit:git[1]).
+
If you do not use bare repositories in your workflow, then it may be
beneficial to set `safe.bareRepository` to `explicit` in your global
config. This will protect you from attacks that involve cloning a
repository that contains a bare repository and running a Git command
within that directory.
+
This config setting is only respected in protected configuration (see
<<SCOPES>>). This prevents the untrusted repository from tampering with
this value.
safe.directory:: safe.directory::
These config entries specify Git-tracked directories that are These config entries specify Git-tracked directories that are
considered safe even if they are owned by someone other than the considered safe even if they are owned by someone other than the

57
setup.c
View File

@ -10,6 +10,10 @@
static int inside_git_dir = -1; static int inside_git_dir = -1;
static int inside_work_tree = -1; static int inside_work_tree = -1;
static int work_tree_config_is_bogus; static int work_tree_config_is_bogus;
enum allowed_bare_repo {
ALLOWED_BARE_REPO_EXPLICIT = 0,
ALLOWED_BARE_REPO_ALL,
};
static struct startup_info the_startup_info; static struct startup_info the_startup_info;
struct startup_info *startup_info = &the_startup_info; struct startup_info *startup_info = &the_startup_info;
@ -1160,6 +1164,46 @@ static int ensure_valid_ownership(const char *gitfile,
return data.is_safe; return data.is_safe;
} }
static int allowed_bare_repo_cb(const char *key, const char *value, void *d)
{
enum allowed_bare_repo *allowed_bare_repo = d;
if (strcasecmp(key, "safe.bareRepository"))
return 0;
if (!strcmp(value, "explicit")) {
*allowed_bare_repo = ALLOWED_BARE_REPO_EXPLICIT;
return 0;
}
if (!strcmp(value, "all")) {
*allowed_bare_repo = ALLOWED_BARE_REPO_ALL;
return 0;
}
return -1;
}
static enum allowed_bare_repo get_allowed_bare_repo(void)
{
enum allowed_bare_repo result = ALLOWED_BARE_REPO_ALL;
git_protected_config(allowed_bare_repo_cb, &result);
return result;
}
static const char *allowed_bare_repo_to_string(
enum allowed_bare_repo allowed_bare_repo)
{
switch (allowed_bare_repo) {
case ALLOWED_BARE_REPO_EXPLICIT:
return "explicit";
case ALLOWED_BARE_REPO_ALL:
return "all";
default:
BUG("invalid allowed_bare_repo %d",
allowed_bare_repo);
}
return NULL;
}
enum discovery_result { enum discovery_result {
GIT_DIR_NONE = 0, GIT_DIR_NONE = 0,
GIT_DIR_EXPLICIT, GIT_DIR_EXPLICIT,
@ -1169,7 +1213,8 @@ enum discovery_result {
GIT_DIR_HIT_CEILING = -1, GIT_DIR_HIT_CEILING = -1,
GIT_DIR_HIT_MOUNT_POINT = -2, GIT_DIR_HIT_MOUNT_POINT = -2,
GIT_DIR_INVALID_GITFILE = -3, GIT_DIR_INVALID_GITFILE = -3,
GIT_DIR_INVALID_OWNERSHIP = -4 GIT_DIR_INVALID_OWNERSHIP = -4,
GIT_DIR_DISALLOWED_BARE = -5,
}; };
/* /*
@ -1297,6 +1342,8 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
} }
if (is_git_directory(dir->buf)) { if (is_git_directory(dir->buf)) {
if (get_allowed_bare_repo() == ALLOWED_BARE_REPO_EXPLICIT)
return GIT_DIR_DISALLOWED_BARE;
if (!ensure_valid_ownership(NULL, NULL, dir->buf)) if (!ensure_valid_ownership(NULL, NULL, dir->buf))
return GIT_DIR_INVALID_OWNERSHIP; return GIT_DIR_INVALID_OWNERSHIP;
strbuf_addstr(gitdir, "."); strbuf_addstr(gitdir, ".");
@ -1443,6 +1490,14 @@ const char *setup_git_directory_gently(int *nongit_ok)
} }
*nongit_ok = 1; *nongit_ok = 1;
break; break;
case GIT_DIR_DISALLOWED_BARE:
if (!nongit_ok) {
die(_("cannot use bare repository '%s' (safe.bareRepository is '%s')"),
dir.buf,
allowed_bare_repo_to_string(get_allowed_bare_repo()));
}
*nongit_ok = 1;
break;
case GIT_DIR_NONE: case GIT_DIR_NONE:
/* /*
* As a safeguard against setup_git_directory_gently_1 returning * As a safeguard against setup_git_directory_gently_1 returning

54
t/t0035-safe-bare-repository.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/sh
test_description='verify safe.bareRepository checks'
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
pwd="$(pwd)"
expect_accepted () {
git "$@" rev-parse --git-dir
}
expect_rejected () {
test_must_fail git "$@" rev-parse --git-dir 2>err &&
grep -F "cannot use bare repository" err
}
test_expect_success 'setup bare repo in worktree' '
git init outer-repo &&
git init --bare outer-repo/bare-repo
'
test_expect_success 'safe.bareRepository unset' '
expect_accepted -C outer-repo/bare-repo
'
test_expect_success 'safe.bareRepository=all' '
test_config_global safe.bareRepository all &&
expect_accepted -C outer-repo/bare-repo
'
test_expect_success 'safe.bareRepository=explicit' '
test_config_global safe.bareRepository explicit &&
expect_rejected -C outer-repo/bare-repo
'
test_expect_success 'safe.bareRepository in the repository' '
# safe.bareRepository must not be "explicit", otherwise
# git config fails with "fatal: not in a git directory" (like
# safe.directory)
test_config -C outer-repo/bare-repo safe.bareRepository \
all &&
test_config_global safe.bareRepository explicit &&
expect_rejected -C outer-repo/bare-repo
'
test_expect_success 'safe.bareRepository on the command line' '
test_config_global safe.bareRepository explicit &&
expect_accepted -C outer-repo/bare-repo \
-c safe.bareRepository=all
'
test_done