Skip to content

Commit 6a24e7b

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 9022eb3 + f8db0bd commit 6a24e7b

File tree

8 files changed

+126
-4
lines changed

8 files changed

+126
-4
lines changed

Documentation/config/http.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ http.sslBackend::
216216
This option is ignored if cURL lacks support for choosing the SSL
217217
backend at runtime.
218218

219+
http.allowNTLMAuth::
220+
Whether or not to allow NTLM authentication. While very convenient to set
221+
up, and therefore still used in many on-prem scenarios, NTLM is a weak
222+
authentication method and therefore deprecated. Defaults to "false".
223+
219224
http.schannelCheckRevoke::
220225
Used to enforce or disable certificate revocation checks in cURL
221226
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
@@ -175,6 +175,9 @@ struct credential {
175175
struct credential_capability capa_authtype;
176176
struct credential_capability capa_state;
177177

178+
unsigned ntlm_suppressed:1,
179+
ntlm_allow:1;
180+
178181
char *username;
179182
char *password;
180183
char *credential;

http.c

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

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

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

646656
credential_fill(&http_auth, 1);
647657

658+
if (http_auth.ntlm_allow && !(http_auth_methods & CURLAUTH_NTLM)) {
659+
http_auth_methods |= CURLAUTH_NTLM;
660+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_methods);
661+
}
662+
648663
if (http_auth.password) {
649664
if (always_auth_proactively()) {
650665
/*
@@ -704,11 +719,11 @@ static void init_curl_proxy_auth(CURL *result)
704719
if (i == ARRAY_SIZE(proxy_authmethods)) {
705720
warning("unsupported proxy authentication method %s: using anyauth",
706721
http_proxy_authmethod);
707-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
722+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
708723
}
709724
}
710725
else
711-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
726+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
712727
}
713728

714729
static int has_cert_password(void)
@@ -1091,7 +1106,7 @@ static CURL *get_curl_handle(void)
10911106
#endif
10921107

10931108
curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
1094-
curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
1109+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_any);
10951110

10961111
#ifdef CURLGSSAPI_DELEGATION_FLAG
10971112
if (curl_deleg) {
@@ -1461,6 +1476,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
14611476
ssl_cert_password_required = 1;
14621477
}
14631478

1479+
http_auth_methods = http_auth_any;
1480+
14641481
curl_default = get_curl_handle();
14651482
}
14661483

@@ -1894,6 +1911,12 @@ static int handle_curl_result(struct slot_results *results)
18941911
} else if (missing_target(results))
18951912
return HTTP_MISSING_TARGET;
18961913
else if (results->http_code == 401) {
1914+
http_auth.ntlm_suppressed = (results->auth_avail & CURLAUTH_NTLM) &&
1915+
!(http_auth_any & CURLAUTH_NTLM);
1916+
if (http_auth.ntlm_suppressed && http_auth.ntlm_allow) {
1917+
http_auth_methods |= CURLAUTH_NTLM;
1918+
return HTTP_REAUTH;
1919+
}
18971920
if ((http_auth.username && http_auth.password) ||\
18981921
(http_auth.authtype && http_auth.credential)) {
18991922
if (http_auth.multistage) {
@@ -1903,6 +1926,16 @@ static int handle_curl_result(struct slot_results *results)
19031926
credential_reject(&http_auth);
19041927
if (always_auth_proactively())
19051928
http_proactive_auth = PROACTIVE_AUTH_NONE;
1929+
if (http_auth.ntlm_suppressed) {
1930+
warning(_("Due to its cryptographic weaknesses, "
1931+
"NTLM authentication has been\n"
1932+
"disabled in Git by default. You can "
1933+
"re-enable it for trusted servers\n"
1934+
"by running:\n\n"
1935+
"git config set "
1936+
"http.%s://%s.allowNTLMAuth true"),
1937+
http_auth.protocol, http_auth.host);
1938+
}
19061939
return HTTP_NOAUTH;
19071940
} else {
19081941
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-perl.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_perl/(.*) apply-one-time-perl.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
@@ -675,4 +675,33 @@ test_expect_success 'access using three-legged auth' '
675675
EOF
676676
'
677677

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

0 commit comments

Comments
 (0)