]> git.ipfire.org Git - thirdparty/git.git/commitdiff
setup.c: create `safe.bareRepository`
authorGlen Choo <chooglen@google.com>
Thu, 14 Jul 2022 21:28:01 +0000 (21:28 +0000)
committerJunio C Hamano <gitster@pobox.com>
Thu, 14 Jul 2022 22:08:29 +0000 (15:08 -0700)
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>
Documentation/config/safe.txt
setup.c
t/t0035-safe-bare-repository.sh [new file with mode: 0755]

index f72b4408798e27549146c99436b955f905de4580..bde7f31459b98136a31a7eb1d457c5c43fddea54 100644 (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::
        These config entries specify Git-tracked directories that are
        considered safe even if they are owned by someone other than the
diff --git a/setup.c b/setup.c
index ec5b9139e32c117252f734e9e6c2ffe621bd8749..8c683e92b62e76cfbc350c8d8f4e9372f62c5f8f 100644 (file)
--- a/setup.c
+++ b/setup.c
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
 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;
 struct startup_info *startup_info = &the_startup_info;
@@ -1160,6 +1164,46 @@ static int ensure_valid_ownership(const char *gitfile,
        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 {
        GIT_DIR_NONE = 0,
        GIT_DIR_EXPLICIT,
@@ -1169,7 +1213,8 @@ enum discovery_result {
        GIT_DIR_HIT_CEILING = -1,
        GIT_DIR_HIT_MOUNT_POINT = -2,
        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 (get_allowed_bare_repo() == ALLOWED_BARE_REPO_EXPLICIT)
+                               return GIT_DIR_DISALLOWED_BARE;
                        if (!ensure_valid_ownership(NULL, NULL, dir->buf))
                                return GIT_DIR_INVALID_OWNERSHIP;
                        strbuf_addstr(gitdir, ".");
@@ -1443,6 +1490,14 @@ const char *setup_git_directory_gently(int *nongit_ok)
                }
                *nongit_ok = 1;
                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:
                /*
                 * As a safeguard against setup_git_directory_gently_1 returning
diff --git a/t/t0035-safe-bare-repository.sh b/t/t0035-safe-bare-repository.sh
new file mode 100755 (executable)
index 0000000..ecbdc82
--- /dev/null
@@ -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