]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Sync with 2.34.7
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Mon, 6 Feb 2023 08:29:44 +0000 (09:29 +0100)
committerJohannes Schindelin <johannes.schindelin@gmx.de>
Mon, 6 Feb 2023 08:29:44 +0000 (09:29 +0100)
* maint-2.34:
  Git 2.34.7
  http: support CURLOPT_PROTOCOLS_STR
  http: prefer CURLOPT_SEEKFUNCTION to CURLOPT_IOCTLFUNCTION
  http-push: prefer CURLOPT_UPLOAD to CURLOPT_PUT
  Git 2.33.7
  Git 2.32.6
  Git 2.31.7
  Git 2.30.8
  apply: fix writing behind newly created symbolic links
  dir-iterator: prevent top-level symlinks without FOLLOW_SYMLINKS
  clone: delay picking a transport until after get_repo_path()
  t5619: demonstrate clone_local() with ambiguous transport

19 files changed:
Documentation/RelNotes/2.30.8.txt [new file with mode: 0644]
Documentation/RelNotes/2.31.7.txt [new file with mode: 0644]
Documentation/RelNotes/2.32.6.txt [new file with mode: 0644]
Documentation/RelNotes/2.33.7.txt [new file with mode: 0644]
Documentation/RelNotes/2.34.7.txt [new file with mode: 0644]
INSTALL
apply.c
builtin/clone.c
dir-iterator.c
dir-iterator.h
git-curl-compat.h
http-push.c
http.c
http.h
remote-curl.c
t/t0066-dir-iterator.sh
t/t4115-apply-symlink.sh
t/t5604-clone-reference.sh
t/t5619-clone-local-ambiguous-transport.sh [new file with mode: 0755]

diff --git a/Documentation/RelNotes/2.30.8.txt b/Documentation/RelNotes/2.30.8.txt
new file mode 100644 (file)
index 0000000..38c23e0
--- /dev/null
@@ -0,0 +1,52 @@
+Git v2.30.8 Release Notes
+=========================
+
+This release addresses the security issues CVE-2023-22490 and
+CVE-2023-23946.
+
+
+Fixes since v2.30.7
+-------------------
+
+ * CVE-2023-22490:
+
+   Using a specially-crafted repository, Git can be tricked into using
+   its local clone optimization even when using a non-local transport.
+   Though Git will abort local clones whose source $GIT_DIR/objects
+   directory contains symbolic links (c.f., CVE-2022-39253), the objects
+   directory itself may still be a symbolic link.
+
+   These two may be combined to include arbitrary files based on known
+   paths on the victim's filesystem within the malicious repository's
+   working copy, allowing for data exfiltration in a similar manner as
+   CVE-2022-39253.
+
+ * CVE-2023-23946:
+
+   By feeding a crafted input to "git apply", a path outside the
+   working tree can be overwritten as the user who is running "git
+   apply".
+
+ * A mismatched type in `attr.c::read_attr_from_index()` which could
+   cause Git to errantly reject attributes on Windows and 32-bit Linux
+   has been corrected.
+
+Credit for finding CVE-2023-22490 goes to yvvdwf, and the fix was
+developed by Taylor Blau, with additional help from others on the
+Git security mailing list.
+
+Credit for finding CVE-2023-23946 goes to Joern Schneeweisz, and the
+fix was developed by Patrick Steinhardt.
+
+
+Johannes Schindelin (1):
+      attr: adjust a mismatched data type
+
+Patrick Steinhardt (1):
+      apply: fix writing behind newly created symbolic links
+
+Taylor Blau (3):
+      t5619: demonstrate clone_local() with ambiguous transport
+      clone: delay picking a transport until after get_repo_path()
+      dir-iterator: prevent top-level symlinks without FOLLOW_SYMLINKS
+
diff --git a/Documentation/RelNotes/2.31.7.txt b/Documentation/RelNotes/2.31.7.txt
new file mode 100644 (file)
index 0000000..dd44d5b
--- /dev/null
@@ -0,0 +1,6 @@
+Git v2.31.7 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.30.8 to
+address the security issues CVE-2023-22490 and CVE-2023-23946;
+see the release notes for that version for details.
diff --git a/Documentation/RelNotes/2.32.6.txt b/Documentation/RelNotes/2.32.6.txt
new file mode 100644 (file)
index 0000000..fd65961
--- /dev/null
@@ -0,0 +1,6 @@
+Git v2.32.6 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.30.8 and v2.31.7
+to address the security issues CVE-2023-22490 and CVE-2023-23946;
+see the release notes for these versions for details.
diff --git a/Documentation/RelNotes/2.33.7.txt b/Documentation/RelNotes/2.33.7.txt
new file mode 100644 (file)
index 0000000..078a837
--- /dev/null
@@ -0,0 +1,7 @@
+Git v2.33.7 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.30.8, v2.31.7
+and v2.32.6 to address the security issues CVE-2023-22490 and
+CVE-2023-23946; see the release notes for these versions for
+details.
diff --git a/Documentation/RelNotes/2.34.7.txt b/Documentation/RelNotes/2.34.7.txt
new file mode 100644 (file)
index 0000000..88898ad
--- /dev/null
@@ -0,0 +1,7 @@
+Git v2.34.7 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.30.8, v2.31.7,
+v2.32.6 and v2.33.7 to address the security issues CVE-2023-22490
+and CVE-2023-23946; see the release notes for these versions
+for details.
diff --git a/INSTALL b/INSTALL
index 4140a3f5c8b6946ca821c2a876cd4390a1a05f1b..8dd577e1020bcca6351abde0d876cc2dfa11493c 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -144,7 +144,7 @@ Issues of note:
          not need that functionality, use NO_CURL to build without
          it.
 
-         Git requires version "7.19.4" or later of "libcurl" to build
+         Git requires version "7.19.5" or later of "libcurl" to build
          without NO_CURL. This version requirement may be bumped in
          the future.
 
diff --git a/apply.c b/apply.c
index 7ffadc3b17a314095e213ad31ceda83a9915afb2..fc6f484a9fd4cfc8cc07e5557bf1a319fbae4fb4 100644 (file)
--- a/apply.c
+++ b/apply.c
@@ -4426,6 +4426,33 @@ static int create_one_file(struct apply_state *state,
        if (state->cached)
                return 0;
 
+       /*
+        * We already try to detect whether files are beyond a symlink in our
+        * up-front checks. But in the case where symlinks are created by any
+        * of the intermediate hunks it can happen that our up-front checks
+        * didn't yet see the symlink, but at the point of arriving here there
+        * in fact is one. We thus repeat the check for symlinks here.
+        *
+        * Note that this does not make the up-front check obsolete as the
+        * failure mode is different:
+        *
+        * - The up-front checks cause us to abort before we have written
+        *   anything into the working directory. So when we exit this way the
+        *   working directory remains clean.
+        *
+        * - The checks here happen in the middle of the action where we have
+        *   already started to apply the patch. The end result will be a dirty
+        *   working directory.
+        *
+        * Ideally, we should update the up-front checks to catch what would
+        * happen when we apply the patch before we damage the working tree.
+        * We have all the information necessary to do so.  But for now, as a
+        * part of embargoed security work, having this check would serve as a
+        * reasonable first step.
+        */
+       if (path_is_beyond_symlink(state, path))
+               return error(_("affected file '%s' is beyond a symbolic link"), path);
+
        res = try_create_file(state, path, mode, buf, size);
        if (res < 0)
                return -1;
index ef6f46b389a9bc7cfcd1c06849dbe0e23afe548a..4541a55013d75ca8d662797fbe2dad9ce7a8d52e 100644 (file)
@@ -1112,10 +1112,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
        refspec_appendf(&remote->fetch, "+%s*:%s*", src_ref_prefix,
                        branch_top.buf);
 
-       transport = transport_get(remote, remote->url[0]);
-       transport_set_verbosity(transport, option_verbosity, option_progress);
-       transport->family = family;
-
        path = get_repo_path(remote->url[0], &is_bundle);
        is_local = option_local != 0 && path && !is_bundle;
        if (is_local) {
@@ -1137,6 +1133,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
        }
        if (option_local > 0 && !is_local)
                warning(_("--local is ignored"));
+
+       transport = transport_get(remote, path ? path : remote->url[0]);
+       transport_set_verbosity(transport, option_verbosity, option_progress);
+       transport->family = family;
        transport->cloning = 1;
 
        transport_set_option(transport, TRANS_OPT_KEEP, "yes");
index b17e9f970a747a3ac85aae81a92c38d2a6491546..3764dd81a185b296a5d4c0a9fc8c0ae2bd4085ec 100644 (file)
@@ -203,7 +203,7 @@ struct dir_iterator *dir_iterator_begin(const char *path, unsigned int flags)
 {
        struct dir_iterator_int *iter = xcalloc(1, sizeof(*iter));
        struct dir_iterator *dir_iterator = &iter->base;
-       int saved_errno;
+       int saved_errno, err;
 
        strbuf_init(&iter->base.path, PATH_MAX);
        strbuf_addstr(&iter->base.path, path);
@@ -213,10 +213,15 @@ struct dir_iterator *dir_iterator_begin(const char *path, unsigned int flags)
        iter->flags = flags;
 
        /*
-        * Note: stat already checks for NULL or empty strings and
-        * inexistent paths.
+        * Note: stat/lstat already checks for NULL or empty strings and
+        * nonexistent paths.
         */
-       if (stat(iter->base.path.buf, &iter->base.st) < 0) {
+       if (iter->flags & DIR_ITERATOR_FOLLOW_SYMLINKS)
+               err = stat(iter->base.path.buf, &iter->base.st);
+       else
+               err = lstat(iter->base.path.buf, &iter->base.st);
+
+       if (err < 0) {
                saved_errno = errno;
                goto error_out;
        }
index 08229157c638040cb19f7dd32b680fbea45f3965..e3b6ff28007366568d981631d5b312bd849486c0 100644 (file)
  *   not the symlinks themselves, which is the default behavior. Broken
  *   symlinks are ignored.
  *
+ *   Note: setting DIR_ITERATOR_FOLLOW_SYMLINKS affects resolving the
+ *   starting path as well (e.g., attempting to iterate starting at a
+ *   symbolic link pointing to a directory without FOLLOW_SYMLINKS will
+ *   result in an error).
+ *
  * Warning: circular symlinks are also followed when
  * DIR_ITERATOR_FOLLOW_SYMLINKS is set. The iteration may end up with
  * an ELOOP if they happen and DIR_ITERATOR_PEDANTIC is set.
index 56a83b6bbd8c43291c94907e74795a46781927bc..fd96b3cdffdb6cd11b429bcada40663e80dbebc9 100644 (file)
 #define GIT_CURL_HAVE_CURLSSLSET_NO_BACKENDS
 #endif
 
+/**
+ * CURLOPT_PROTOCOLS_STR and CURLOPT_REDIR_PROTOCOLS_STR were added in 7.85.0,
+ * released in August 2022.
+ */
+#if LIBCURL_VERSION_NUM >= 0x075500
+#define GIT_CURL_HAVE_CURLOPT_PROTOCOLS_STR 1
+#endif
+
 #endif
index 3309aaf004a4db175bc4c0b94b5c63efebcb30e3..b4aeae9e2695369c648ba615e182ab5c3f08561b 100644 (file)
@@ -198,13 +198,13 @@ static void curl_setup_http(CURL *curl, const char *url,
                const char *custom_req, struct buffer *buffer,
                curl_write_callback write_fn)
 {
-       curl_easy_setopt(curl, CURLOPT_PUT, 1);
+       curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_INFILE, buffer);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE, buffer->buf.len);
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, fread_buffer);
-       curl_easy_setopt(curl, CURLOPT_IOCTLFUNCTION, ioctl_buffer);
-       curl_easy_setopt(curl, CURLOPT_IOCTLDATA, buffer);
+       curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, seek_buffer);
+       curl_easy_setopt(curl, CURLOPT_SEEKDATA, buffer);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_fn);
        curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, custom_req);
diff --git a/http.c b/http.c
index 229da4d14882d9c9855ab418ad64f30fe62e485a..2a97d1773557710a1ebb8ac02ed72c2fc52d318d 100644 (file)
--- a/http.c
+++ b/http.c
@@ -155,21 +155,19 @@ size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
        return size / eltsize;
 }
 
-curlioerr ioctl_buffer(CURL *handle, int cmd, void *clientp)
+int seek_buffer(void *clientp, curl_off_t offset, int origin)
 {
        struct buffer *buffer = clientp;
 
-       switch (cmd) {
-       case CURLIOCMD_NOP:
-               return CURLIOE_OK;
-
-       case CURLIOCMD_RESTARTREAD:
-               buffer->posn = 0;
-               return CURLIOE_OK;
-
-       default:
-               return CURLIOE_UNKNOWNCMD;
+       if (origin != SEEK_SET)
+               BUG("seek_buffer only handles SEEK_SET");
+       if (offset < 0 || offset >= buffer->buf.len) {
+               error("curl seek would be outside of buffer");
+               return CURL_SEEKFUNC_FAIL;
        }
+
+       buffer->posn = offset;
+       return CURL_SEEKFUNC_OK;
 }
 
 size_t fwrite_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
@@ -717,20 +715,37 @@ void setup_curl_trace(CURL *handle)
        curl_easy_setopt(handle, CURLOPT_DEBUGDATA, NULL);
 }
 
-static long get_curl_allowed_protocols(int from_user)
+static void proto_list_append(struct strbuf *list, const char *proto)
 {
-       long allowed_protocols = 0;
+       if (!list)
+               return;
+       if (list->len)
+               strbuf_addch(list, ',');
+       strbuf_addstr(list, proto);
+}
 
-       if (is_transport_allowed("http", from_user))
-               allowed_protocols |= CURLPROTO_HTTP;
-       if (is_transport_allowed("https", from_user))
-               allowed_protocols |= CURLPROTO_HTTPS;
-       if (is_transport_allowed("ftp", from_user))
-               allowed_protocols |= CURLPROTO_FTP;
-       if (is_transport_allowed("ftps", from_user))
-               allowed_protocols |= CURLPROTO_FTPS;
+static long get_curl_allowed_protocols(int from_user, struct strbuf *list)
+{
+       long bits = 0;
 
-       return allowed_protocols;
+       if (is_transport_allowed("http", from_user)) {
+               bits |= CURLPROTO_HTTP;
+               proto_list_append(list, "http");
+       }
+       if (is_transport_allowed("https", from_user)) {
+               bits |= CURLPROTO_HTTPS;
+               proto_list_append(list, "https");
+       }
+       if (is_transport_allowed("ftp", from_user)) {
+               bits |= CURLPROTO_FTP;
+               proto_list_append(list, "ftp");
+       }
+       if (is_transport_allowed("ftps", from_user)) {
+               bits |= CURLPROTO_FTPS;
+               proto_list_append(list, "ftps");
+       }
+
+       return bits;
 }
 
 #ifdef GIT_CURL_HAVE_CURL_HTTP_VERSION_2
@@ -874,10 +889,26 @@ static CURL *get_curl_handle(void)
 
        curl_easy_setopt(result, CURLOPT_MAXREDIRS, 20);
        curl_easy_setopt(result, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
+
+#ifdef GIT_CURL_HAVE_CURLOPT_PROTOCOLS_STR
+       {
+               struct strbuf buf = STRBUF_INIT;
+
+               get_curl_allowed_protocols(0, &buf);
+               curl_easy_setopt(result, CURLOPT_REDIR_PROTOCOLS_STR, buf.buf);
+               strbuf_reset(&buf);
+
+               get_curl_allowed_protocols(-1, &buf);
+               curl_easy_setopt(result, CURLOPT_PROTOCOLS_STR, buf.buf);
+               strbuf_release(&buf);
+       }
+#else
        curl_easy_setopt(result, CURLOPT_REDIR_PROTOCOLS,
-                        get_curl_allowed_protocols(0));
+                        get_curl_allowed_protocols(0, NULL));
        curl_easy_setopt(result, CURLOPT_PROTOCOLS,
-                        get_curl_allowed_protocols(-1));
+                        get_curl_allowed_protocols(-1, NULL));
+#endif
+
        if (getenv("GIT_CURL_VERBOSE"))
                http_trace_curl_no_data();
        setup_curl_trace(result);
diff --git a/http.h b/http.h
index df1590e53a455787a2d4d28a7896cabf8ac15419..77e0520582dfc94af8d0eeb875355e6dfb09b524 100644 (file)
--- a/http.h
+++ b/http.h
@@ -40,7 +40,7 @@ struct buffer {
 size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *strbuf);
 size_t fwrite_buffer(char *ptr, size_t eltsize, size_t nmemb, void *strbuf);
 size_t fwrite_null(char *ptr, size_t eltsize, size_t nmemb, void *strbuf);
-curlioerr ioctl_buffer(CURL *handle, int cmd, void *clientp);
+int seek_buffer(void *clientp, curl_off_t offset, int origin);
 
 /* Slot lifecycle functions */
 struct active_request_slot *get_active_slot(void);
index 0dabef2dd7c565f3f2c2ecd74a0ca295bd874afb..a5a678c042d9cce43a551752d40d8f416e46cf39 100644 (file)
@@ -710,25 +710,23 @@ static size_t rpc_out(void *ptr, size_t eltsize,
        return avail;
 }
 
-static curlioerr rpc_ioctl(CURL *handle, int cmd, void *clientp)
+static int rpc_seek(void *clientp, curl_off_t offset, int origin)
 {
        struct rpc_state *rpc = clientp;
 
-       switch (cmd) {
-       case CURLIOCMD_NOP:
-               return CURLIOE_OK;
+       if (origin != SEEK_SET)
+               BUG("rpc_seek only handles SEEK_SET, not %d", origin);
 
-       case CURLIOCMD_RESTARTREAD:
-               if (rpc->initial_buffer) {
-                       rpc->pos = 0;
-                       return CURLIOE_OK;
+       if (rpc->initial_buffer) {
+               if (offset < 0 || offset > rpc->len) {
+                       error("curl seek would be outside of rpc buffer");
+                       return CURL_SEEKFUNC_FAIL;
                }
-               error(_("unable to rewind rpc post data - try increasing http.postBuffer"));
-               return CURLIOE_FAILRESTART;
-
-       default:
-               return CURLIOE_UNKNOWNCMD;
+               rpc->pos = offset;
+               return CURL_SEEKFUNC_OK;
        }
+       error(_("unable to rewind rpc post data - try increasing http.postBuffer"));
+       return CURL_SEEKFUNC_FAIL;
 }
 
 struct check_pktline_state {
@@ -948,8 +946,8 @@ retry:
                rpc->initial_buffer = 1;
                curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, rpc_out);
                curl_easy_setopt(slot->curl, CURLOPT_INFILE, rpc);
-               curl_easy_setopt(slot->curl, CURLOPT_IOCTLFUNCTION, rpc_ioctl);
-               curl_easy_setopt(slot->curl, CURLOPT_IOCTLDATA, rpc);
+               curl_easy_setopt(slot->curl, CURLOPT_SEEKFUNCTION, rpc_seek);
+               curl_easy_setopt(slot->curl, CURLOPT_SEEKDATA, rpc);
                if (options.verbosity > 1) {
                        fprintf(stderr, "POST %s (chunked)\n", rpc->service_name);
                        fflush(stderr);
index 63a1a45cd30921a5b0d28b8d5db63046aeb2a3ea..04b811622bc4b1299095d7663928400679fc56be 100755 (executable)
@@ -110,7 +110,9 @@ test_expect_success SYMLINKS 'setup dirs with symlinks' '
        mkdir -p dir5/a/c &&
        ln -s ../c dir5/a/b/d &&
        ln -s ../ dir5/a/b/e &&
-       ln -s ../../ dir5/a/b/f
+       ln -s ../../ dir5/a/b/f &&
+
+       ln -s dir4 dir6
 '
 
 test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default' '
@@ -146,4 +148,27 @@ test_expect_success SYMLINKS 'dir-iterator should follow symlinks w/ follow flag
        test_cmp expected-follow-sorted-output actual-follow-sorted-output
 '
 
+test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '
+       test_must_fail test-tool dir-iterator ./dir6 >out &&
+
+       grep "ENOTDIR" out
+'
+
+test_expect_success SYMLINKS 'dir-iterator resolves top-level symlinks w/ follow flag' '
+       cat >expected-follow-sorted-output <<-EOF &&
+       [d] (a) [a] ./dir6/a
+       [d] (a/f) [f] ./dir6/a/f
+       [d] (a/f/c) [c] ./dir6/a/f/c
+       [d] (b) [b] ./dir6/b
+       [d] (b/c) [c] ./dir6/b/c
+       [f] (a/d) [d] ./dir6/a/d
+       [f] (a/e) [e] ./dir6/a/e
+       EOF
+
+       test-tool dir-iterator --follow-symlinks ./dir6 >out &&
+       sort out >actual-follow-sorted-output &&
+
+       test_cmp expected-follow-sorted-output actual-follow-sorted-output
+'
+
 test_done
index d0f3edef54acf6247a034041eb234c73f67d5762..65ac7df2d745c4f7a698baf23cc71c2a80221c3e 100755 (executable)
@@ -45,4 +45,85 @@ test_expect_success 'apply --index symlink patch' '
 
 '
 
+test_expect_success 'symlink setup' '
+       ln -s .git symlink &&
+       git add symlink &&
+       git commit -m "add symlink"
+'
+
+test_expect_success SYMLINKS 'symlink escape when creating new files' '
+       test_when_finished "git reset --hard && git clean -dfx" &&
+
+       cat >patch <<-EOF &&
+       diff --git a/symlink b/renamed-symlink
+       similarity index 100%
+       rename from symlink
+       rename to renamed-symlink
+       --
+       diff --git /dev/null b/renamed-symlink/create-me
+       new file mode 100644
+       index 0000000..039727e
+       --- /dev/null
+       +++ b/renamed-symlink/create-me
+       @@ -0,0 +1,1 @@
+       +busted
+       EOF
+
+       test_must_fail git apply patch 2>stderr &&
+       cat >expected_stderr <<-EOF &&
+       error: affected file ${SQ}renamed-symlink/create-me${SQ} is beyond a symbolic link
+       EOF
+       test_cmp expected_stderr stderr &&
+       ! test_path_exists .git/create-me
+'
+
+test_expect_success SYMLINKS 'symlink escape when modifying file' '
+       test_when_finished "git reset --hard && git clean -dfx" &&
+       touch .git/modify-me &&
+
+       cat >patch <<-EOF &&
+       diff --git a/symlink b/renamed-symlink
+       similarity index 100%
+       rename from symlink
+       rename to renamed-symlink
+       --
+       diff --git a/renamed-symlink/modify-me b/renamed-symlink/modify-me
+       index 1111111..2222222 100644
+       --- a/renamed-symlink/modify-me
+       +++ b/renamed-symlink/modify-me
+       @@ -0,0 +1,1 @@
+       +busted
+       EOF
+
+       test_must_fail git apply patch 2>stderr &&
+       cat >expected_stderr <<-EOF &&
+       error: renamed-symlink/modify-me: No such file or directory
+       EOF
+       test_cmp expected_stderr stderr &&
+       test_must_be_empty .git/modify-me
+'
+
+test_expect_success SYMLINKS 'symlink escape when deleting file' '
+       test_when_finished "git reset --hard && git clean -dfx && rm .git/delete-me" &&
+       touch .git/delete-me &&
+
+       cat >patch <<-EOF &&
+       diff --git a/symlink b/renamed-symlink
+       similarity index 100%
+       rename from symlink
+       rename to renamed-symlink
+       --
+       diff --git a/renamed-symlink/delete-me b/renamed-symlink/delete-me
+       deleted file mode 100644
+       index 1111111..0000000 100644
+       EOF
+
+       test_must_fail git apply patch 2>stderr &&
+       cat >expected_stderr <<-EOF &&
+       error: renamed-symlink/delete-me: No such file or directory
+       EOF
+       test_cmp expected_stderr stderr &&
+       test_path_is_file .git/delete-me
+'
+
 test_done
index 2734e37e8804cd6944301e1d836af517fb1172b5..7ccebb40c33825561618fce95afc2e96b0349f6f 100755 (executable)
@@ -344,4 +344,20 @@ test_expect_success SYMLINKS 'clone repo with symlinked or unknown files at obje
        test_must_be_empty T--shared.objects-symlinks.raw
 '
 
+test_expect_success SYMLINKS 'clone repo with symlinked objects directory' '
+       test_when_finished "rm -fr sensitive malicious" &&
+
+       mkdir -p sensitive &&
+       echo "secret" >sensitive/file &&
+
+       git init malicious &&
+       rm -fr malicious/.git/objects &&
+       ln -s "$(pwd)/sensitive" ./malicious/.git/objects &&
+
+       test_must_fail git clone --local malicious clone 2>err &&
+
+       test_path_is_missing clone &&
+       grep "failed to start iterator over" err
+'
+
 test_done
diff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh
new file mode 100755 (executable)
index 0000000..cce62bf
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+test_description='test local clone with ambiguous transport'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-httpd.sh"
+
+if ! test_have_prereq SYMLINKS
+then
+       skip_all='skipping test, symlink support unavailable'
+       test_done
+fi
+
+start_httpd
+
+REPO="$HTTPD_DOCUMENT_ROOT_PATH/sub.git"
+URI="$HTTPD_URL/dumb/sub.git"
+
+test_expect_success 'setup' '
+       mkdir -p sensitive &&
+       echo "secret" >sensitive/secret &&
+
+       git init --bare "$REPO" &&
+       test_commit_bulk -C "$REPO" --ref=main 1 &&
+
+       git -C "$REPO" update-ref HEAD main &&
+       git -C "$REPO" update-server-info &&
+
+       git init malicious &&
+       (
+               cd malicious &&
+
+               git submodule add "$URI" &&
+
+               mkdir -p repo/refs &&
+               touch repo/refs/.gitkeep &&
+               printf "ref: refs/heads/a" >repo/HEAD &&
+               ln -s "$(cd .. && pwd)/sensitive" repo/objects &&
+
+               mkdir -p "$HTTPD_URL/dumb" &&
+               ln -s "../../../.git/modules/sub/../../../repo/" "$URI" &&
+
+               git add . &&
+               git commit -m "initial commit"
+       ) &&
+
+       # Delete all of the references in our malicious submodule to
+       # avoid the client attempting to checkout any objects (which
+       # will be missing, and thus will cause the clone to fail before
+       # we can trigger the exploit).
+       git -C "$REPO" for-each-ref --format="delete %(refname)" >in &&
+       git -C "$REPO" update-ref --stdin <in &&
+       git -C "$REPO" update-server-info
+'
+
+test_expect_success 'ambiguous transport does not lead to arbitrary file-inclusion' '
+       git clone malicious clone &&
+       test_must_fail git -C clone submodule update --init 2>err &&
+
+       test_path_is_missing clone/.git/modules/sub/objects/secret &&
+       # We would actually expect "transport .file. not allowed" here,
+       # but due to quirks of the URL detection in Git, we mis-parse
+       # the absolute path as a bogus URL and die before that step.
+       #
+       # This works for now, and if we ever fix the URL detection, it
+       # is OK to change this to detect the transport error.
+       grep "protocol .* is not supported" err
+'
+
+test_done