[PATCH] bundle-uri: refuse advertised URIs by protocol
"Johannes Schindelin via GitGitGadget" <[email protected]>
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <[email protected]> |
From: Johannes Schindelin <[email protected]> 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 <[email protected]> --- 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