Skip to content

Commit 3255d0a

Browse files
dschoGit for Windows Build Agent
authored andcommitted
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 f8313b7 + 4815781 commit 3255d0a

File tree

8 files changed

+129
-4
lines changed

8 files changed

+129
-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: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ enum http_follow_config http_follow_config = HTTP_FOLLOW_INITIAL;
131131

132132
static struct credential cert_auth = CREDENTIAL_INIT;
133133
static int ssl_cert_password_required;
134-
static unsigned long http_auth_methods = CURLAUTH_ANY;
134+
static unsigned long http_auth_any = CURLAUTH_ANY & ~CURLAUTH_NTLM;
135+
static unsigned long http_auth_methods;
135136
static int http_auth_methods_restricted;
136137
/* Modes for which empty_auth cannot actually help us. */
137138
static unsigned long empty_auth_useless =
@@ -437,6 +438,15 @@ static int http_options(const char *var, const char *value,
437438
return 0;
438439
}
439440

441+
if (!strcmp("http.allowntlmauth", var)) {
442+
if (git_config_bool(var, value)) {
443+
http_auth_any |= CURLAUTH_NTLM;
444+
} else {
445+
http_auth_any &= ~CURLAUTH_NTLM;
446+
}
447+
return 0;
448+
}
449+
440450
if (!strcmp("http.schannelcheckrevoke", var)) {
441451
if (value && !strcmp(value, "best-effort")) {
442452
http_schannel_check_revoke_mode =
@@ -675,6 +685,11 @@ static void init_curl_http_auth(CURL *result)
675685

676686
credential_fill(the_repository, &http_auth, 1);
677687

688+
if (http_auth.ntlm_allow && !(http_auth_methods & CURLAUTH_NTLM)) {
689+
http_auth_methods |= CURLAUTH_NTLM;
690+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_methods);
691+
}
692+
678693
if (http_auth.password) {
679694
if (always_auth_proactively()) {
680695
/*
@@ -750,11 +765,11 @@ static void init_curl_proxy_auth(CURL *result)
750765
if (i == ARRAY_SIZE(proxy_authmethods)) {
751766
warning("unsupported proxy authentication method %s: using anyauth",
752767
http_proxy_authmethod);
753-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
768+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
754769
}
755770
}
756771
else
757-
curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
772+
curl_easy_setopt(result, CURLOPT_PROXYAUTH, http_auth_any);
758773
}
759774

760775
static int has_cert_password(void)
@@ -1101,7 +1116,7 @@ static CURL *get_curl_handle(void)
11011116
}
11021117

11031118
curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
1104-
curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
1119+
curl_easy_setopt(result, CURLOPT_HTTPAUTH, http_auth_any);
11051120

11061121
#ifdef CURLGSSAPI_DELEGATION_FLAG
11071122
if (curl_deleg) {
@@ -1500,6 +1515,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
15001515
set_long_from_env(&http_max_retries, "GIT_HTTP_MAX_RETRIES");
15011516
set_long_from_env(&http_max_retry_time, "GIT_HTTP_MAX_RETRY_TIME");
15021517

1518+
http_auth_methods = http_auth_any;
1519+
15031520
curl_default = get_curl_handle();
15041521
}
15051522

@@ -1931,6 +1948,8 @@ static int handle_curl_result(struct slot_results *results)
19311948
} else if (missing_target(results))
19321949
return HTTP_MISSING_TARGET;
19331950
else if (results->http_code == 401) {
1951+
http_auth.ntlm_suppressed = (results->auth_avail & CURLAUTH_NTLM) &&
1952+
!(http_auth_any & CURLAUTH_NTLM);
19341953
if ((http_auth.username && http_auth.password) ||\
19351954
(http_auth.authtype && http_auth.credential)) {
19361955
if (http_auth.multistage) {
@@ -1940,6 +1959,16 @@ static int handle_curl_result(struct slot_results *results)
19401959
credential_reject(the_repository, &http_auth);
19411960
if (always_auth_proactively())
19421961
http_proactive_auth = PROACTIVE_AUTH_NONE;
1962+
if (http_auth.ntlm_suppressed) {
1963+
warning(_("Due to its cryptographic weaknesses, "
1964+
"NTLM authentication has been\n"
1965+
"disabled in Git by default. You can "
1966+
"re-enable it for trusted servers\n"
1967+
"by running:\n\n"
1968+
"git config set "
1969+
"http.%s://%s.allowNTLMAuth true"),
1970+
http_auth.protocol, http_auth.host);
1971+
}
19431972
return HTTP_NOAUTH;
19441973
} else {
19451974
if (curl_empty_auth == -1 &&
@@ -2464,6 +2493,13 @@ static int http_request_recoverable(const char *url,
24642493
http_reauth_prepare(1);
24652494
}
24662495

2496+
/*
2497+
* Re-enable NTLM auth if the helper allows it and we would
2498+
* otherwise suppress authentication via NTLM.
2499+
*/
2500+
if (http_auth.ntlm_suppressed && http_auth.ntlm_allow)
2501+
http_auth_methods |= CURLAUTH_NTLM;
2502+
24672503
ret = http_request(url, result, target, options);
24682504
}
24692505
if (ret == HTTP_RATE_LIMITED) {

t/lib-httpd.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ prepare_httpd() {
168168
install_script apply-one-time-script.sh
169169
install_script nph-custom-auth.sh
170170
install_script http-429.sh
171+
install_script ntlm-handshake.sh
171172

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

t/lib-httpd/apache.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ SetEnv PERL_PATH ${PERL_PATH}
155155
CGIPassAuth on
156156
</IfDefine>
157157
</LocationMatch>
158+
<LocationMatch /ntlm_auth/>
159+
SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH}
160+
SetEnv GIT_HTTP_EXPORT_ALL
161+
<IfDefine USE_CGIPASSAUTH>
162+
CGIPassAuth on
163+
</IfDefine>
164+
</LocationMatch>
158165
ScriptAlias /smart/incomplete_length/git-upload-pack incomplete-length-upload-pack-v2-http.sh/
159166
ScriptAlias /smart/incomplete_body/git-upload-pack incomplete-body-upload-pack-v2-http.sh/
160167
ScriptAlias /smart/no_report/git-receive-pack error-no-report.sh/
@@ -166,6 +173,7 @@ ScriptAlias /error/ error.sh/
166173
ScriptAliasMatch /one_time_script/(.*) apply-one-time-script.sh/$1
167174
ScriptAliasMatch /http_429/(.*) http-429.sh/$1
168175
ScriptAliasMatch /custom_auth/(.*) nph-custom-auth.sh/$1
176+
ScriptAliasMatch /ntlm_auth/(.*) ntlm-handshake.sh/$1
169177
<Directory ${GIT_EXEC_PATH}>
170178
Options FollowSymlinks
171179
</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
@@ -793,4 +793,33 @@ test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '
793793
test_line_count = 1 actual_401s
794794
'
795795

796+
test_lazy_prereq NTLM 'curl --version | grep -q NTLM'
797+
798+
test_expect_success NTLM 'access using NTLM auth' '
799+
test_when_finished "per_test_cleanup" &&
800+
801+
set_credential_reply get <<-EOF &&
802+
username=user
803+
password=pwd
804+
EOF
805+
806+
test_config_global credential.helper test-helper &&
807+
test_must_fail env GIT_TRACE_CURL=1 git \
808+
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" 2>err &&
809+
test_grep "allowNTLMAuth" err &&
810+
811+
# Can be enabled via config
812+
GIT_TRACE_CURL=1 git -c http.$HTTPD_URL.allowNTLMAuth=true \
813+
ls-remote "$HTTPD_URL/ntlm_auth/repo.git" &&
814+
815+
# Or via credential helper responding with ntlm=allow
816+
set_credential_reply get <<-EOF &&
817+
username=user
818+
password=pwd
819+
ntlm=allow
820+
EOF
821+
822+
git ls-remote "$HTTPD_URL/ntlm_auth/repo.git"
823+
'
824+
796825
test_done

0 commit comments

Comments
 (0)