From d019447c020d6b4bb756d0353298cb3897abf42f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 26 Jun 2026 12:15:27 +0200 Subject: [PATCH] bundle-uri: refuse advertised URIs by protocol 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 --- 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 2bb2eb99e47b58..92a36ca0ab3a43 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 @@ int fetch_bundle_uri(struct repository *r, const char *uri, 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 794478ae194c8d..889e673a442f77 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" +'