From 76524c2604bceee48481909e7824494498c39a34 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Mon, 28 Sep 2026 00:01:49 -0400 Subject: [PATCH 1/2] http: handle curl stripping creds from effective url When we detect that curl performed a redirect of a URL we requested, we update our base URL to match the new location and flush the http_auth credentials. This goes back to c93c92f309 (http: update base URLs when we see redirects, 2013-09-28). We detect the redirect by comparing the requested URL to the response from CURLINFO_EFFECTIVE_URL, using a simple string comparison. This has worked fine for years, but a change in the upcoming curl 8.23.0 adds a complication. If our URL directly contains credentials (like "https://user:pass@example.com/foo.git"), then as of 7a6bd027d0 (getinfo: make sure CURLINFO_EFFECTIVE_URL does not contain creds, 2026-09-21), curl will strip the credentials from what it returns (so just "https://example.com/foo.git" in this case). This breaks our direct string comparison, and we believe that we've been redirected. We flush our http_auth credentials, and now subsequent requests will use the reduced URL, causing us to re-request credentials from the user. Notably this causes t5550.15 (among others) to complain; it tries a clone with credentials in the URL, and fails if the user is prompted at all. We can handle this new behavior by doing a more careful comparison: if the direct string comparison fails, we'll strip out the credentials ourselves and compare. This is a little extra work, but in practice it should only happen once per process. I've used curl's curl_url() interface to do the stripping here, mostly because its behavior should match the stripping it does internally. And also, though we have code to parse a URL, we don't have any to reconstruct it, making a single string comparison hard. One alternative would be to parse with url_parse() or similar, and compare the individual fields (skipping username/password). I think that would probably also work in practice, but it seemed to me that the simplest change would be sticking with string comparisons. The curl_url() interface appeared in 7.62.0. We document that 7.61.0 is still supported, so I've made it conditional here. Only new versions strip the result from CURLINFO_EFFECTIVE_URL, so it's OK for very old versions to skip the extra comparison. Likewise if we encounter any errors, we just quietly skip the comparison. That's fine if you don't have creds in your URLs, and if you do, you'll get end up in the existing error path (a redirect warning, and eventually an auth failure). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- git-curl-compat.h | 7 +++++++ http.c | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/git-curl-compat.h b/git-curl-compat.h index 032aaf7126c977..25678b5dbd2c80 100644 --- a/git-curl-compat.h +++ b/git-curl-compat.h @@ -28,6 +28,13 @@ * introduced, oldest first, in the official version of cURL library. */ +/** + * curl_url() interface added in 7.62.0 (October 2018) + */ +#if LIBCURL_VERSION_NUM >= 0x073e00 +#define GIT_CURL_HAVE_CURL_URL +#endif + /** * Versions before curl 7.66.0 (September 2019) required manually setting the * transfer-encoding for a streaming POST; after that this is handled diff --git a/http.c b/http.c index c8fcfd7693e897..4af3c29c769559 100644 --- a/http.c +++ b/http.c @@ -2315,6 +2315,45 @@ static int http_request(const char *url, return ret; } +#ifndef GIT_CURL_HAVE_CURL_URL +#define strip_url_credential(in) NULL +#else +static char *strip_url_credential(const char *in) +{ + char *ret = NULL; + CURLU *url; + + url = curl_url(); + if (!url) + goto out; + + if (curl_url_set(url, CURLUPART_URL, in, 0)) + goto out; + + curl_url_set(url, CURLUPART_USER, NULL, 0); + curl_url_set(url, CURLUPART_PASSWORD, NULL, 0); + curl_url_get(url, CURLUPART_URL, &ret, 0); + +out: + curl_url_cleanup(url); + return ret; +} +#endif + +static int match_effective_url(const char *asked, const char *got) +{ + char *stripped; + int ret; + + if (!strcmp(asked, got)) + return 1; + + stripped = strip_url_credential(asked); + ret = stripped && !strcmp(stripped, got); + curl_free(stripped); + return ret; +} + /* * Update the "base" url to a more appropriate value, as deduced by * redirects seen when requesting a URL starting with "url". @@ -2347,7 +2386,7 @@ static int update_url_from_redirect(struct strbuf *base, const char *tail; size_t new_len; - if (!strcmp(asked, got->buf)) + if (match_effective_url(asked, got->buf)) return 0; if (!skip_prefix(asked, base->buf, &tail)) From 3b832f0ff18080edb67556ff085826f3b65b1c5d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 15 Oct 2026 13:40:30 +0200 Subject: [PATCH 2/2] fixup! http: optionally load libcurl lazily The `jk/http-curl-strip-creds` topic introduces callers to yet-uncovered cURL library functions, which we hereby cover in the lazyload-curl shim. Signed-off-by: Johannes Schindelin --- compat/lazyload-curl.c | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/compat/lazyload-curl.c b/compat/lazyload-curl.c index a6a3f7e3a7aeaa..c361eab85cd1fb 100644 --- a/compat/lazyload-curl.c +++ b/compat/lazyload-curl.c @@ -175,6 +175,20 @@ static curl_easy_setopt_pointer_type curl_easy_setopt_pointer_func; typedef CURLcode (*curl_easy_setopt_off_t_type)(CURL *curl, CURLoption opt, curl_off_t value); static curl_easy_setopt_off_t_type curl_easy_setopt_off_t_func; +typedef CURLU *(*curl_url_type)(void); +static curl_url_type curl_url_func; + +typedef void (*curl_url_cleanup_type)(CURLU *u); +static curl_url_cleanup_type curl_url_cleanup_func; + +typedef CURLUcode (*curl_url_get_type)(const CURLU *u, CURLUPart what, + char **part, unsigned int flags); +static curl_url_get_type curl_url_get_func; + +typedef CURLUcode (*curl_url_set_type)(CURLU *u, CURLUPart what, + const char *part, unsigned int flags); +static curl_url_set_type curl_url_set_func; + static char ssl_backend[64]; static void lazy_load_curl(void) @@ -231,6 +245,11 @@ static void lazy_load_curl(void) curl_easy_setopt_long_func = (curl_easy_setopt_long_type)curl_easy_setopt_func; curl_easy_setopt_pointer_func = (curl_easy_setopt_pointer_type)curl_easy_setopt_func; curl_easy_setopt_off_t_func = (curl_easy_setopt_off_t_type)curl_easy_setopt_func; + + curl_url_func = (curl_url_type)load_function(libcurl, "curl_url"); + curl_url_cleanup_func = (curl_url_cleanup_type)load_function(libcurl, "curl_url_cleanup"); + curl_url_get_func = (curl_url_get_type)load_function(libcurl, "curl_url_get"); + curl_url_set_func = (curl_url_set_type)load_function(libcurl, "curl_url_set"); } struct curl_version_info_data *curl_version_info(CURLversion version) @@ -426,3 +445,25 @@ CURLcode curl_easy_setopt(CURL *curl, CURLoption opt, ...) va_end(ap); return res; } + +CURLU *curl_url(void) +{ + return curl_url_func(); +} + +void curl_url_cleanup(CURLU *u) +{ + curl_url_cleanup_func(u); +} + +CURLUcode curl_url_get(const CURLU *u, CURLUPart what, + char **part, unsigned int flags) +{ + return curl_url_get_func(u, what, part, flags); +} + +CURLUcode curl_url_set(CURLU *u, CURLUPart what, + const char *part, unsigned int flags) +{ + return curl_url_set_func(u, what, part, flags); +}