Merge branch 'disallow-ntlm-auth-by-default'

This topic branch addresses the following vulnerability:

- **CVE-2025-66413**:
  When a user clones a repository from an attacker-controlled server,
  Git may attempt NTLM authentication and disclose the user's NTLMv2 hash
  to the remote server. Since NTLM hashing is weak, the captured hash can
  potentially be brute-forced to recover the user's credentials. This is
  addressed by disabling NTLM authentication by default.
  (https://github.com/git-for-windows/git/security/advisories/GHSA-hv9c-4jm9-jh3x)

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This commit is contained in:
Johannes Schindelin
2026-02-12 16:06:29 +01:00
committed by Git for Windows Build Agent
8 changed files with 126 additions and 4 deletions

View File

@@ -231,6 +231,11 @@ http.sslKeyType::
See also libcurl `CURLOPT_SSLKEYTYPE`. Can be overridden by the
`GIT_SSL_KEY_TYPE` environment variable.
http.allowNTLMAuth::
Whether or not to allow NTLM authentication. While very convenient to set
up, and therefore still used in many on-prem scenarios, NTLM is a weak
authentication method and therefore deprecated. Defaults to "false".
http.schannelCheckRevoke::
Used to enforce or disable certificate revocation checks in cURL
when http.sslBackend is set to "schannel" via "true" and "false",

View File

@@ -360,6 +360,9 @@ int credential_read(struct credential *c, FILE *fp,
credential_set_capability(&c->capa_authtype, op_type);
else if (!strcmp(value, "state"))
credential_set_capability(&c->capa_state, op_type);
} else if (!strcmp(key, "ntlm")) {
if (!strcmp(value, "allow"))
c->ntlm_allow = 1;
} else if (!strcmp(key, "continue")) {
c->multistage = !!git_config_bool("continue", value);
} else if (!strcmp(key, "password_expiry_utc")) {
@@ -420,6 +423,8 @@ void credential_write(const struct credential *c, FILE *fp,
if (c->ephemeral)
credential_write_item(c, fp, "ephemeral", "1", 0);
}
if (c->ntlm_suppressed)
credential_write_item(c, fp, "ntlm", "suppressed", 0);
credential_write_item(c, fp, "protocol", c->protocol, 1);
credential_write_item(c, fp, "host", c->host, 1);
credential_write_item(c, fp, "path", c->path, 0);

View File

@@ -177,6 +177,9 @@ struct credential {
struct credential_capability capa_authtype;
struct credential_capability capa_state;
unsigned ntlm_suppressed:1,
ntlm_allow:1;
char *username;
char *password;
char *credential;

41
http.c
View File

@@ -131,7 +131,8 @@ enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;
static struct credential cert_auth = CREDENTIAL_INIT;
static int ssl_cert_password_required;
static unsigned long http_auth_methods = CURLAUTH_ANY;
static unsigned long http_auth_any = CURLAUTH_ANY & ~CURLAUTH_NTLM;
static unsigned long http_auth_methods;
static int http_auth_methods_restricted;
/* Modes for which empty_auth cannot actually help us. */
static unsigned long empty_auth_useless =
@@ -436,6 +437,15 @@ static int http_options(const char *var, const char *value,
return 0;
}
if (!strcmp("http.allowntlmauth", var)) {
if (git_config_bool(var, value)) {
http_auth_any |= CURLAUTH_NTLM;
} else {
http_auth_any &= ~CURLAUTH_NTLM;
}
return 0;
}
if (!strcmp("http.schannelcheckrevoke", var)) {
if (value && !strcmp(value, "best-effort")) {
http_schannel_check_revoke_mode =
@@ -674,6 +684,11 @@ static void init_curl_http_auth(CURL *result)
credential_fill(the_repository, &http_auth, 1);
if (http_auth.ntlm_allow && !(http_auth_methods & CURLAUTH_NTLM)) {
http_auth_methods |= CURLAUTH_NTLM;
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_methods);
}
if (http_auth.password) {
if (always_auth_proactively()) {
/*
@@ -733,11 +748,11 @@ static void init_curl_proxy_auth(CURL *result)
if (i == ARRAY_SIZE(proxy_authmethods)) {
warning("unsupported proxy authentication method %s: using anyauth",
http_proxy_authmethod);
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
}
}
else
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
}
static int has_cert_password(void)
@@ -1084,7 +1099,7 @@ static CURL *get_curl_handle(void)
}
curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_any);
#ifdef CURLGSSAPI_DELEGATION_FLAG
if (curl_deleg) {
@@ -1483,6 +1498,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
set_long_from_env(&http_max_retries, "GIT_HTTP_MAX_RETRIES");
set_long_from_env(&http_max_retry_time, "GIT_HTTP_MAX_RETRY_TIME");
http_auth_methods = http_auth_any;
curl_default = get_curl_handle();
}
@@ -1914,6 +1931,12 @@ static int handle_curl_result(struct slot_results *results)
} else if (missing_target(results))
return HTTP_MISSING_TARGET;
else if (results->http_code == 401) {
http_auth.ntlm_suppressed = (results->auth_avail & CURLAUTH_NTLM) &&
!(http_auth_any & CURLAUTH_NTLM);
if (http_auth.ntlm_suppressed && http_auth.ntlm_allow) {
http_auth_methods |= CURLAUTH_NTLM;
return HTTP_REAUTH;
}
if ((http_auth.username && http_auth.password) ||\
(http_auth.authtype && http_auth.credential)) {
if (http_auth.multistage) {
@@ -1923,6 +1946,16 @@ static int handle_curl_result(struct slot_results *results)
credential_reject(the_repository, &http_auth);
if (always_auth_proactively())
http_proactive_auth = PROACTIVE_AUTH_NONE;
if (http_auth.ntlm_suppressed) {
warning(_("Due to its cryptographic weaknesses, "
"NTLM authentication has been\n"
"disabled in Git by default. You can "
"re-enable it for trusted servers\n"
"by running:\n\n"
"git config set "
"http.%s://%s.allowNTLMAuth true"),
http_auth.protocol, http_auth.host);
}
return HTTP_NOAUTH;
} else {
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;

View File

@@ -168,6 +168,7 @@ prepare_httpd() {
install_script apply-one-time-script.sh
install_script nph-custom-auth.sh
install_script http-429.sh
install_script ntlm-handshake.sh
ln -s "$LIB_HTTPD_MODULE_PATH" "$HTTPD_ROOT_PATH/modules"

View File

@@ -155,6 +155,13 @@ SetEnv PERL_PATH ${PERL_PATH}
CGIPassAuth on
</IfDefine>
</LocationMatch>
<LocationMatch /ntlm_auth/>
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
SetEnv GIT_HTTP_EXPORT_ALL
<IfDefine USE_CGIPASSAUTH>
CGIPassAuth on
</IfDefine>
</LocationMatch>
ScriptAlias /smart/incomplete_length/git-upload-pack incomplete-length-upload-pack-v2-http.sh/
ScriptAlias /smart/incomplete_body/git-upload-pack incomplete-body-upload-pack-v2-http.sh/
ScriptAlias /smart/no_report/git-receive-pack error-no-report.sh/
@@ -166,6 +173,7 @@ ScriptAlias /error/ error.sh/
ScriptAliasMatch /one_time_script/(.*) apply-one-time-script.sh/$1
ScriptAliasMatch /http_429/(.*) http-429.sh/$1
ScriptAliasMatch /custom_auth/(.*) nph-custom-auth.sh/$1
ScriptAliasMatch /ntlm_auth/(.*) ntlm-handshake.sh/$1
<Directory ${GIT_EXEC_PATH}>
Options FollowSymlinks
</Directory>

38
t/lib-httpd/ntlm-handshake.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/sh
case "$HTTP_AUTHORIZATION" in
'')
# No Authorization header -> send NTLM challenge
echo "Status: 401 Unauthorized"
echo "WWW-Authenticate: NTLM"
echo
;;
"NTLM TlRMTVNTUAAB"*)
# Type 1 -> respond with Type 2 challenge (hardcoded)
echo "Status: 401 Unauthorized"
# Base64-encoded version of the Type 2 challenge:
# signature: 'NTLMSSP\0'
# message_type: 2
# target_name: 'NTLM-GIT-SERVER'
# flags: 0xa2898205 =
# NEGOTIATE_UNICODE, REQUEST_TARGET, NEGOTIATE_NT_ONLY,
# TARGET_TYPE_SERVER, TARGET_TYPE_SHARE, REQUEST_NON_NT_SESSION_KEY,
# NEGOTIATE_VERSION, NEGOTIATE_128, NEGOTIATE_56
# challenge: 0xfa3dec518896295b
# context: '0000000000000000'
# target_info_present: true
# target_info_len: 128
# version: '10.0 (build 19041)'
echo "WWW-Authenticate: NTLM TlRMTVNTUAACAAAAHgAeADgAAAAFgomi+j3sUYiWKVsAAAAAAAAAAIAAgABWAAAACgBhSgAAAA9OAFQATABNAC0ARwBJAFQALQBTAEUAUgBWAEUAUgACABIAVwBPAFIASwBHAFIATwBVAFAAAQAeAE4AVABMAE0ALQBHAEkAVAAtAFMARQBSAFYARQBSAAQAEgBXAE8AUgBLAEcAUgBPAFUAUAADAB4ATgBUAEwATQAtAEcASQBUAC0AUwBFAFIAVgBFAFIABwAIAACfOcZKYNwBAAAAAA=="
echo
;;
"NTLM TlRMTVNTUAAD"*)
# Type 3 -> accept without validation
exec "$GIT_EXEC_PATH"/git-http-backend
;;
*)
echo "Status: 500 Unrecognized"
echo
echo "Unhandled auth: '$HTTP_AUTHORIZATION'"
;;
esac

View File

@@ -719,4 +719,33 @@ test_expect_success 'access using three-legged auth' '
EOF
'
test_lazy_prereq NTLM 'curl --version | grep -q NTLM'
test_expect_success NTLM 'access using NTLM auth' '
test_when_finished "per_test_cleanup" &&
set_credential_reply get <<-EOF &&
username=user
password=pwd
EOF
test_config_global credential.helper test-helper &&
test_must_fail env GIT_TRACE_CURL=1 git \
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" 2>err &&
test_grep "allowNTLMAuth" err &&
# Can be enabled via config
GIT_TRACE_CURL=1 git -c http.$HTTPD_URL.allowNTLMAuth=true \
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" &&
# Or via credential helper responding with ntlm=allow
set_credential_reply get <<-EOF &&
username=user
password=pwd
ntlm=allow
EOF
git ls-remote "$HTTPD_URL/ntlm_auth/repo.git"
'
test_done