Skip to content

Commit 317946c

Browse files
committed
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. (GHSA-hv9c-4jm9-jh3x) Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
2 parents bd2ecba + 816db62 commit 317946c

File tree

8 files changed

+126
-4
lines changed

8 files changed

+126
-4
lines changed

Documentation/config/http.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ http.sslKeyType::
231231
See also libcurl `CURLOPT_SSLKEYTYPE`. Can be overridden by the
232232
`GIT_SSL_KEY_TYPE` environment variable.
233233

234+
http.allowNTLMAuth::
235+
Whether or not to allow NTLM authentication. While very convenient to set
236+
up, and therefore still used in many on-prem scenarios, NTLM is a weak
237+
authentication method and therefore deprecated. Defaults to "false".
238+
234239
http.schannelCheckRevoke::
235240
Used to enforce or disable certificate revocation checks in cURL
236241
when http.sslBackend is set to "schannel" via "true" and "false",

credential.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ int credential_read(struct credential *c, FILE *fp,
360360
credential_set_capability(&c->capa_authtype, op_type);
361361
else if (!strcmp(value, "state"))
362362
credential_set_capability(&c->capa_state, op_type);
363+
} else if (!strcmp(key, "ntlm")) {
364+
if (!strcmp(value, "allow"))
365+
c->ntlm_allow = 1;
363366
} else if (!strcmp(key, "continue")) {
364367
c->multistage = !!git_config_bool("continue", value);
365368
} else if (!strcmp(key, "password_expiry_utc")) {
@@ -420,6 +423,8 @@ void credential_write(const struct credential *c, FILE *fp,
420423
if (c->ephemeral)
421424
credential_write_item(c, fp, "ephemeral", "1", 0);
422425
}
426+
if (c->ntlm_suppressed)
427+
credential_write_item(c, fp, "ntlm", "suppressed", 0);
423428
credential_write_item(c, fp, "protocol", c->protocol, 1);
424429
credential_write_item(c, fp, "host", c->host, 1);
425430
credential_write_item(c, fp, "path", c->path, 0);

credential.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ struct credential {
177177
struct credential_capability capa_authtype;
178178
struct credential_capability capa_state;
179179

180+
unsigned ntlm_suppressed:1,
181+
ntlm_allow:1;
182+
180183
char *username;
181184
char *password;
182185
char *credential;

http.c

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;
129129

130130
static struct credential cert_auth = CREDENTIAL_INIT;
131131
static int ssl_cert_password_required;
132-
static unsigned long http_auth_methods = CURLAUTH_ANY;
132+
static unsigned long http_auth_any = CURLAUTH_ANY & ~CURLAUTH_NTLM;
133+
static unsigned long http_auth_methods;
133134
static int http_auth_methods_restricted;
134135
/* Modes for which empty_auth cannot actually help us. */
135136
static unsigned long empty_auth_useless =
@@ -430,6 +431,15 @@ static int http_options(const char *var, const char *value,
430431
return 0;
431432
}
432433

434+
if (!strcmp("http.allowntlmauth", var)) {
435+
if (git_config_bool(var, value)) {
436+
http_auth_any |= CURLAUTH_NTLM;
437+
} else {
438+
http_auth_any &= ~CURLAUTH_NTLM;
439+
}
440+
return 0;
441+
}
442+
433443
if (!strcmp("http.schannelcheckrevoke", var)) {
434444
if (value && !strcmp(value, "best-effort")) {
435445
http_schannel_check_revoke_mode =
@@ -653,6 +663,11 @@ static void init_curl_http_auth(CURL *result)
653663

654664
credential_fill(the_repository, &http_auth, 1);
655665

666+
if (http_auth.ntlm_allow && !(http_auth_methods & CURLAUTH_NTLM)) {
667+
http_auth_methods |= CURLAUTH_NTLM;
668+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_methods);
669+
}
670+
656671
if (http_auth.password) {
657672
if (always_auth_proactively()) {
658673
/*
@@ -712,11 +727,11 @@ static void init_curl_proxy_auth(CURL *result)
712727
if (i == ARRAY_SIZE(proxy_authmethods)) {
713728
warning("unsupported proxy authentication method %s: using anyauth",
714729
http_proxy_authmethod);
715-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
730+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
716731
}
717732
}
718733
else
719-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
734+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
720735
}
721736

722737
static int has_cert_password(void)
@@ -1063,7 +1078,7 @@ static CURL *get_curl_handle(void)
10631078
}
10641079

10651080
curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
1066-
curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
1081+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_any);
10671082

10681083
#ifdef CURLGSSAPI_DELEGATION_FLAG
10691084
if (curl_deleg) {
@@ -1458,6 +1473,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14581473
set_long_from_env(&curl_tcp_keepintvl, "GIT_TCP_KEEPINTVL");
14591474
set_long_from_env(&curl_tcp_keepcnt, "GIT_TCP_KEEPCNT");
14601475

1476+
http_auth_methods = http_auth_any;
1477+
14611478
curl_default = get_curl_handle();
14621479
}
14631480

@@ -1889,6 +1906,12 @@ static int handle_curl_result(struct slot_results *results)
18891906
} else if (missing_target(results))
18901907
return HTTP_MISSING_TARGET;
18911908
else if (results->http_code == 401) {
1909+
http_auth.ntlm_suppressed = (results->auth_avail & CURLAUTH_NTLM) &&
1910+
!(http_auth_any & CURLAUTH_NTLM);
1911+
if (http_auth.ntlm_suppressed && http_auth.ntlm_allow) {
1912+
http_auth_methods |= CURLAUTH_NTLM;
1913+
return HTTP_REAUTH;
1914+
}
18921915
if ((http_auth.username && http_auth.password) ||\
18931916
(http_auth.authtype && http_auth.credential)) {
18941917
if (http_auth.multistage) {
@@ -1898,6 +1921,16 @@ static int handle_curl_result(struct slot_results *results)
18981921
credential_reject(the_repository, &http_auth);
18991922
if (always_auth_proactively())
19001923
http_proactive_auth = PROACTIVE_AUTH_NONE;
1924+
if (http_auth.ntlm_suppressed) {
1925+
warning(_("Due to its cryptographic weaknesses, "
1926+
"NTLM authentication has been\n"
1927+
"disabled in Git by default. You can "
1928+
"re-enable it for trusted servers\n"
1929+
"by running:\n\n"
1930+
"git config set "
1931+
"http.%s://%s.allowNTLMAuth true"),
1932+
http_auth.protocol, http_auth.host);
1933+
}
19011934
return HTTP_NOAUTH;
19021935
} else {
19031936
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;

t/lib-httpd.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ prepare_httpd() {
167167
install_script error.sh
168168
install_script apply-one-time-script.sh
169169
install_script nph-custom-auth.sh
170+
install_script ntlm-handshake.sh
170171

171172
ln -s "$LIB_HTTPD_MODULE_PATH" "$HTTPD_ROOT_PATH/modules"
172173

t/lib-httpd/apache.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ SetEnv PERL_PATH ${PERL_PATH}
151151
CGIPassAuth on
152152
</IfDefine>
153153
</LocationMatch>
154+
<LocationMatch /ntlm_auth/>
155+
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
156+
SetEnv GIT_HTTP_EXPORT_ALL
157+
<IfDefine USE_CGIPASSAUTH>
158+
CGIPassAuth on
159+
</IfDefine>
160+
</LocationMatch>
154161
ScriptAlias /smart/incomplete_length/git-upload-pack incomplete-length-upload-pack-v2-http.sh/
155162
ScriptAlias /smart/incomplete_body/git-upload-pack incomplete-body-upload-pack-v2-http.sh/
156163
ScriptAlias /smart/no_report/git-receive-pack error-no-report.sh/
@@ -161,6 +168,7 @@ ScriptAlias /error_smart/ error-smart-http.sh/
161168
ScriptAlias /error/ error.sh/
162169
ScriptAliasMatch /one_time_script/(.*) apply-one-time-script.sh/$1
163170
ScriptAliasMatch /custom_auth/(.*) nph-custom-auth.sh/$1
171+
ScriptAliasMatch /ntlm_auth/(.*) ntlm-handshake.sh/$1
164172
<Directory ${GIT_EXEC_PATH}>
165173
Options FollowSymlinks
166174
</Directory>

t/lib-httpd/ntlm-handshake.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/sh
2+
3+
case "$HTTP_AUTHORIZATION" in
4+
'')
5+
# No Authorization header -> send NTLM challenge
6+
echo "Status: 401 Unauthorized"
7+
echo "WWW-Authenticate: NTLM"
8+
echo
9+
;;
10+
"NTLM TlRMTVNTUAAB"*)
11+
# Type 1 -> respond with Type 2 challenge (hardcoded)
12+
echo "Status: 401 Unauthorized"
13+
# Base64-encoded version of the Type 2 challenge:
14+
# signature: 'NTLMSSP\0'
15+
# message_type: 2
16+
# target_name: 'NTLM-GIT-SERVER'
17+
# flags: 0xa2898205 =
18+
# NEGOTIATE_UNICODE, REQUEST_TARGET, NEGOTIATE_NT_ONLY,
19+
# TARGET_TYPE_SERVER, TARGET_TYPE_SHARE, REQUEST_NON_NT_SESSION_KEY,
20+
# NEGOTIATE_VERSION, NEGOTIATE_128, NEGOTIATE_56
21+
# challenge: 0xfa3dec518896295b
22+
# context: '0000000000000000'
23+
# target_info_present: true
24+
# target_info_len: 128
25+
# version: '10.0 (build 19041)'
26+
echo "WWW-Authenticate: NTLM TlRMTVNTUAACAAAAHgAeADgAAAAFgomi+j3sUYiWKVsAAAAAAAAAAIAAgABWAAAACgBhSgAAAA9OAFQATABNAC0ARwBJAFQALQBTAEUAUgBWAEUAUgACABIAVwBPAFIASwBHAFIATwBVAFAAAQAeAE4AVABMAE0ALQBHAEkAVAAtAFMARQBSAFYARQBSAAQAEgBXAE8AUgBLAEcAUgBPAFUAUAADAB4ATgBUAEwATQAtAEcASQBUAC0AUwBFAFIAVgBFAFIABwAIAACfOcZKYNwBAAAAAA=="
27+
echo
28+
;;
29+
"NTLM TlRMTVNTUAAD"*)
30+
# Type 3 -> accept without validation
31+
exec "$GIT_EXEC_PATH"/git-http-backend
32+
;;
33+
*)
34+
echo "Status: 500 Unrecognized"
35+
echo
36+
echo "Unhandled auth: '$HTTP_AUTHORIZATION'"
37+
;;
38+
esac

t/t5563-simple-http-auth.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,4 +674,33 @@ test_expect_success 'access using three-legged auth' '
674674
EOF
675675
'
676676

677+
test_lazy_prereq NTLM 'curl --version | grep -q NTLM'
678+
679+
test_expect_success NTLM 'access using NTLM auth' '
680+
test_when_finished "per_test_cleanup" &&
681+
682+
set_credential_reply get <<-EOF &&
683+
username=user
684+
password=pwd
685+
EOF
686+
687+
test_config_global credential.helper test-helper &&
688+
test_must_fail env GIT_TRACE_CURL=1 git \
689+
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" 2>err &&
690+
test_grep "allowNTLMAuth" err &&
691+
692+
# Can be enabled via config
693+
GIT_TRACE_CURL=1 git -c http.$HTTPD_URL.allowNTLMAuth=true \
694+
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" &&
695+
696+
# Or via credential helper responding with ntlm=allow
697+
set_credential_reply get <<-EOF &&
698+
username=user
699+
password=pwd
700+
ntlm=allow
701+
EOF
702+
703+
git ls-remote "$HTTPD_URL/ntlm_auth/repo.git"
704+
'
705+
677706
test_done

0 commit comments

Comments
 (0)