All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Johannes Schindelin <johannes.schindelin@gmx.de>,
	Johannes Schindelin <johannes.schindelin@gmx.de>
Subject: [PATCH] bundle-uri: refuse advertised URIs by protocol
Date: Wed, 12 Aug 2026 15:55:56 +0000	[thread overview]
Message-ID: <pull.2200.git.1786550157424.gitgitgadget@gmail.com> (raw)

From: Johannes Schindelin <johannes.schindelin@gmx.de>

Servers may advertise bundle URIs that are not HTTP(S);
copy_uri_to_file() then opens it as a local path. On Windows that can be
a UNC path like `//attacker/share/x`, i.e. a clone can be manipulated
into making an outbound SMB connection that leaks NTLM credentials
(CVE-2026-62960).

Subject advertised URIs to the usual protocol allow-list
(`protocol.*.allow`), which drops "file" (and bare/UNC paths) by default
but keeps http/https/git/ssh. Do it in fetch_bundle_list(), the
clone/fetch consume path, so ls-remote still lists everything; each
skipped URI is reported. A user-supplied `--bundle-uri` is unaffected,
and `protocol.file.allow=always` re-enables an advertised file URI.

Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
    bundle-uri: refuse advertised URIs by protocol
    
    This is the security fix released with Git for Windows v2.55.0(4). Due
    to the transparent NTLM authentication ("SSPI"), the vulnerability
    affects only Windows. The patch has been sent to the git-security list
    on June 26th, 2026, but only received reviews in the PR in
    https://github.com/git-for-windows/git/security/advisories/GHSA-xrpg-8j9v-v282's
    private fork (which had to be deleted so that the advisory could be
    published).

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2200%2Fdscho%2Frespect-allowed-protocols-in-bundle-uris-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2200/dscho/respect-allowed-protocols-in-bundle-uris-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2200

 bundle-uri.c                 | 50 ++++++++++++++++++++++++++++++++
 t/lib-bundle-uri-protocol.sh | 56 ++++++++++++++++++++++++++++++++++++
 2 files changed, 106 insertions(+)

diff --git a/bundle-uri.c b/bundle-uri.c
index 2bb2eb99e4..92a36ca0ab 100644
--- a/bundle-uri.c
+++ b/bundle-uri.c
@@ -15,6 +15,8 @@
 #include "remote.h"
 #include "trace2.h"
 #include "odb.h"
+#include "transport.h"
+#include "url.h"
 
 static struct {
 	enum bundle_list_heuristic heuristic;
@@ -890,11 +892,59 @@ cleanup:
 	return result;
 }
 
+/* protocol of 'uri', or "file" if it has none (bare/UNC/relative path) */
+static void bundle_uri_protocol(const char *uri, struct strbuf *out)
+{
+	const char *p = uri;
+
+	while (is_urlschemechar(p == uri, *p))
+		p++;
+	strbuf_reset(out);
+	if (p > uri && starts_with(p, "://"))
+		strbuf_add(out, uri, p - uri);
+	else
+		strbuf_addstr(out, "file");
+}
+
+/* Drop advertised URIs whose protocol is not allowed (see protocol.*.allow). */
+static void sanitize_bundle_list(struct bundle_list *list)
+{
+	struct remote_bundle_info **skipped;
+	size_t nr = 0, i;
+	struct remote_bundle_info *info;
+	struct hashmap_iter iter;
+	struct strbuf proto = STRBUF_INIT;
+
+	ALLOC_ARRAY(skipped, hashmap_get_size(&list->bundles));
+	hashmap_for_each_entry(&list->bundles, &iter, info, ent) {
+		if (!info->uri)
+			continue;
+		bundle_uri_protocol(info->uri, &proto);
+		/* advertised URIs are not user-provided */
+		if (!is_transport_allowed(proto.buf, 0)) {
+			warning(_("skipping bundle URI '%s': protocol '%s' "
+				  "is not allowed"), info->uri, proto.buf);
+			skipped[nr++] = info;
+		}
+	}
+	strbuf_release(&proto);
+
+	for (i = 0; i < nr; i++) {
+		hashmap_remove(&list->bundles, &skipped[i]->ent, NULL);
+		clear_remote_bundle_info(skipped[i], NULL);
+		free(skipped[i]);
+	}
+
+	free(skipped);
+}
+
 int fetch_bundle_list(struct repository *r, struct bundle_list *list)
 {
 	int result;
 	struct bundle_list global_list;
 
+	sanitize_bundle_list(list);
+
 	/*
 	 * If the creationToken heuristic is used, then the URIs
 	 * advertised by 'list' are not nested lists and instead
diff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh
index 794478ae19..889e673a44 100644
--- a/t/lib-bundle-uri-protocol.sh
+++ b/t/lib-bundle-uri-protocol.sh
@@ -237,3 +237,59 @@ test_expect_success "test bundle-uri with $BUNDLE_URI_PROTOCOL:// using protocol
 		>actual &&
 	test_cmp_config_output expect actual
 '
+
+# Advertised bundle URIs are subject to protocol.*.allow; "file" (and bare or
+# UNC paths) is denied by default, so such a URI must be skipped, not fetched.
+advertise_uri () {
+	test_config -C "$BUNDLE_URI_PARENT" bundle.version 1 &&
+	test_config -C "$BUNDLE_URI_PARENT" bundle.mode all &&
+	test_config -C "$BUNDLE_URI_PARENT" bundle.payload.uri "$1"
+}
+
+ignores_advertised_uri () {
+	rm -rf victim &&
+	advertise_uri "$1" &&
+	git -c transfer.bundleURI=true -c protocol.version=2 \
+		clone "$BUNDLE_URI_REPO_URI" victim &&
+	git -C victim for-each-ref refs/bundles/ >refs &&
+	test_must_be_empty refs
+}
+
+test_expect_success "create bundle to advertise" '
+	git -C "$BUNDLE_URI_PARENT" bundle create "$PWD/payload.bundle" main
+'
+
+test_expect_success "ignore non-HTTP(S) bundle URI with $BUNDLE_URI_PROTOCOL://" '
+	ignores_advertised_uri "$PWD/payload.bundle" &&
+	ignores_advertised_uri "file://$PWD/payload.bundle"
+'
+
+test_expect_success "protocol.file.allow=always honors file bundle URI with $BUNDLE_URI_PROTOCOL://" '
+	rm -rf victim &&
+	advertise_uri "$PWD/payload.bundle" &&
+	git -c transfer.bundleURI=true -c protocol.version=2 \
+		-c protocol.file.allow=always \
+		clone "$BUNDLE_URI_REPO_URI" victim &&
+	git -C victim rev-parse --verify refs/bundles/heads/main
+'
+
+# same path via a UNC administrative share (cf. t5580-unc-paths.sh)
+if test_have_prereq CYGWIN
+then
+	UNCPATH="$(cygpath -aw .)"
+elif test_have_prereq MINGW
+then
+	UNCPATH="$(pwd)"
+fi
+case "$UNCPATH" in
+[A-Za-z]:*)
+	WITHOUTDRIVE="${UNCPATH#?:}"
+	UNCPATH="//localhost/${UNCPATH%%:*}\$$WITHOUTDRIVE"
+	test -d "$UNCPATH" && test_set_prereq ADMIN_UNC
+	;;
+esac
+
+test_expect_success ADMIN_UNC "ignore UNC bundle URI with $BUNDLE_URI_PROTOCOL://" '
+	ignores_advertised_uri "$UNCPATH/payload.bundle" &&
+	ignores_advertised_uri "file://$UNCPATH/payload.bundle"
+'

base-commit: 11c6700f10234578d10523faf35656ca491425c9
-- 
gitgitgadget

                 reply	other threads:[~2026-08-12 15:55 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2200.git.1786550157424.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=johannes.schindelin@gmx.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.