195 lines
8.0 KiB
Diff
195 lines
8.0 KiB
Diff
From 6f054f9fb3a501c35b55c65e547a244f14c38d56 Mon Sep 17 00:00:00 2001
|
|
From: Taylor Blau <me@ttaylorr.com>
|
|
Date: Thu, 28 Jul 2022 17:35:17 -0400
|
|
Subject: [PATCH] builtin/clone.c: disallow `--local` clones with symlinks
|
|
|
|
When cloning a repository with `--local`, Git relies on either making a
|
|
hardlink or copy to every file in the "objects" directory of the source
|
|
repository. This is done through the callpath `cmd_clone()` ->
|
|
`clone_local()` -> `copy_or_link_directory()`.
|
|
|
|
The way this optimization works is by enumerating every file and
|
|
directory recursively in the source repository's `$GIT_DIR/objects`
|
|
directory, and then either making a copy or hardlink of each file. The
|
|
only exception to this rule is when copying the "alternates" file, in
|
|
which case paths are rewritten to be absolute before writing a new
|
|
"alternates" file in the destination repo.
|
|
|
|
One quirk of this implementation is that it dereferences symlinks when
|
|
cloning. This behavior was most recently modified in 36596fd2df (clone:
|
|
better handle symlinked files at .git/objects/, 2019-07-10), which
|
|
attempted to support `--local` clones of repositories with symlinks in
|
|
their objects directory in a platform-independent way.
|
|
|
|
Unfortunately, this behavior of dereferencing symlinks (that is,
|
|
creating a hardlink or copy of the source's link target in the
|
|
destination repository) can be used as a component in attacking a
|
|
victim by inadvertently exposing the contents of file stored outside of
|
|
the repository.
|
|
|
|
Take, for example, a repository that stores a Dockerfile and is used to
|
|
build Docker images. When building an image, Docker copies the directory
|
|
contents into the VM, and then instructs the VM to execute the
|
|
Dockerfile at the root of the copied directory. This protects against
|
|
directory traversal attacks by copying symbolic links as-is without
|
|
dereferencing them.
|
|
|
|
That is, if a user has a symlink pointing at their private key material
|
|
(where the symlink is present in the same directory as the Dockerfile,
|
|
but the key itself is present outside of that directory), the key is
|
|
unreadable to a Docker image, since the link will appear broken from the
|
|
container's point of view.
|
|
|
|
This behavior enables an attack whereby a victim is convinced to clone a
|
|
repository containing an embedded submodule (with a URL like
|
|
"file:///proc/self/cwd/path/to/submodule") which has a symlink pointing
|
|
at a path containing sensitive information on the victim's machine. If a
|
|
user is tricked into doing this, the contents at the destination of
|
|
those symbolic links are exposed to the Docker image at runtime.
|
|
|
|
One approach to preventing this behavior is to recreate symlinks in the
|
|
destination repository. But this is problematic, since symlinking the
|
|
objects directory are not well-supported. (One potential problem is that
|
|
when sharing, e.g. a "pack" directory via symlinks, different writers
|
|
performing garbage collection may consider different sets of objects to
|
|
be reachable, enabling a situation whereby garbage collecting one
|
|
repository may remove reachable objects in another repository).
|
|
|
|
Instead, prohibit the local clone optimization when any symlinks are
|
|
present in the `$GIT_DIR/objects` directory of the source repository.
|
|
Users may clone the repository again by prepending the "file://" scheme
|
|
to their clone URL, or by adding the `--no-local` option to their `git
|
|
clone` invocation.
|
|
|
|
The directory iterator used by `copy_or_link_directory()` must no longer
|
|
dereference symlinks (i.e., it *must* call `lstat()` instead of `stat()`
|
|
in order to discover whether or not there are symlinks present). This has
|
|
no bearing on the overall behavior, since we will immediately `die()` on
|
|
encounter a symlink.
|
|
|
|
Note that t5604.33 suggests that we do support local clones with
|
|
symbolic links in the source repository's objects directory, but this
|
|
was likely unintentional, or at least did not take into consideration
|
|
the problem with sharing parts of the objects directory with symbolic
|
|
links at the time. Update this test to reflect which options are and
|
|
aren't supported.
|
|
|
|
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
|
|
Signed-off-by: Taylor Blau <me@ttaylorr.com>
|
|
---
|
|
builtin/clone.c | 8 +++---
|
|
t/t5604-clone-reference.sh | 50 ++++++++++++++------------------------
|
|
2 files changed, 23 insertions(+), 35 deletions(-)
|
|
|
|
diff --git a/builtin/clone.c b/builtin/clone.c
|
|
index e335734b4c..e626073b1f 100644
|
|
--- a/builtin/clone.c
|
|
+++ b/builtin/clone.c
|
|
@@ -420,13 +420,11 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
|
|
int src_len, dest_len;
|
|
struct dir_iterator *iter;
|
|
int iter_status;
|
|
- unsigned int flags;
|
|
struct strbuf realpath = STRBUF_INIT;
|
|
|
|
mkdir_if_missing(dest->buf, 0777);
|
|
|
|
- flags = DIR_ITERATOR_PEDANTIC | DIR_ITERATOR_FOLLOW_SYMLINKS;
|
|
- iter = dir_iterator_begin(src->buf, flags);
|
|
+ iter = dir_iterator_begin(src->buf, DIR_ITERATOR_PEDANTIC);
|
|
|
|
if (!iter)
|
|
die_errno(_("failed to start iterator over '%s'"), src->buf);
|
|
@@ -442,6 +440,10 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
|
|
strbuf_setlen(dest, dest_len);
|
|
strbuf_addstr(dest, iter->relative_path);
|
|
|
|
+ if (S_ISLNK(iter->st.st_mode))
|
|
+ die(_("symlink '%s' exists, refusing to clone with --local"),
|
|
+ iter->relative_path);
|
|
+
|
|
if (S_ISDIR(iter->st.st_mode)) {
|
|
mkdir_if_missing(dest->buf, 0777);
|
|
continue;
|
|
diff --git a/t/t5604-clone-reference.sh b/t/t5604-clone-reference.sh
|
|
index 2f7be23044..9d32f1c4a4 100755
|
|
--- a/t/t5604-clone-reference.sh
|
|
+++ b/t/t5604-clone-reference.sh
|
|
@@ -300,8 +300,6 @@ test_expect_success SYMLINKS 'setup repo with manually symlinked or unknown file
|
|
ln -s ../an-object $obj &&
|
|
|
|
cd ../ &&
|
|
- find . -type f | sort >../../../T.objects-files.raw &&
|
|
- find . -type l | sort >../../../T.objects-symlinks.raw &&
|
|
echo unknown_content >unknown_file
|
|
) &&
|
|
git -C T fsck &&
|
|
@@ -310,19 +308,27 @@ test_expect_success SYMLINKS 'setup repo with manually symlinked or unknown file
|
|
|
|
|
|
test_expect_success SYMLINKS 'clone repo with symlinked or unknown files at objects/' '
|
|
- for option in --local --no-hardlinks --shared --dissociate
|
|
+ # None of these options work when cloning locally, since T has
|
|
+ # symlinks in its `$GIT_DIR/objects` directory
|
|
+ for option in --local --no-hardlinks --dissociate
|
|
do
|
|
- git clone $option T T$option || return 1 &&
|
|
- git -C T$option fsck || return 1 &&
|
|
- git -C T$option rev-list --all --objects >T$option.objects &&
|
|
- test_cmp T.objects T$option.objects &&
|
|
- (
|
|
- cd T$option/.git/objects &&
|
|
- find . -type f | sort >../../../T$option.objects-files.raw &&
|
|
- find . -type l | sort >../../../T$option.objects-symlinks.raw
|
|
- )
|
|
+ test_must_fail git clone $option T T$option 2>err || return 1 &&
|
|
+ test_i18ngrep "symlink.*exists" err || return 1
|
|
done &&
|
|
|
|
+ # But `--shared` clones should still work, even when specifying
|
|
+ # a local path *and* that repository has symlinks present in its
|
|
+ # `$GIT_DIR/objects` directory.
|
|
+ git clone --shared T T--shared &&
|
|
+ git -C T--shared fsck &&
|
|
+ git -C T--shared rev-list --all --objects >T--shared.objects &&
|
|
+ test_cmp T.objects T--shared.objects &&
|
|
+ (
|
|
+ cd T--shared/.git/objects &&
|
|
+ find . -type f | sort >../../../T--shared.objects-files.raw &&
|
|
+ find . -type l | sort >../../../T--shared.objects-symlinks.raw
|
|
+ ) &&
|
|
+
|
|
for raw in $(ls T*.raw)
|
|
do
|
|
sed -e "s!/../!/Y/!; s![0-9a-f]\{38,\}!Z!" -e "/commit-graph/d" \
|
|
@@ -330,26 +336,6 @@ test_expect_success SYMLINKS 'clone repo with symlinked or unknown files at obje
|
|
sort $raw.de-sha-1 >$raw.de-sha || return 1
|
|
done &&
|
|
|
|
- cat >expected-files <<-EOF &&
|
|
- ./Y/Z
|
|
- ./Y/Z
|
|
- ./Y/Z
|
|
- ./a-loose-dir/Z
|
|
- ./an-object
|
|
- ./info/packs
|
|
- ./pack/pack-Z.idx
|
|
- ./pack/pack-Z.pack
|
|
- ./packs/pack-Z.idx
|
|
- ./packs/pack-Z.pack
|
|
- ./unknown_file
|
|
- EOF
|
|
-
|
|
- for option in --local --no-hardlinks --dissociate
|
|
- do
|
|
- test_cmp expected-files T$option.objects-files.raw.de-sha || return 1 &&
|
|
- test_must_be_empty T$option.objects-symlinks.raw.de-sha || return 1
|
|
- done &&
|
|
-
|
|
echo ./info/alternates >expected-files &&
|
|
test_cmp expected-files T--shared.objects-files.raw &&
|
|
test_must_be_empty T--shared.objects-symlinks.raw
|
|
--
|
|
2.27.0
|
|
|