From 11ed8a20046bf3baff41cd1873a1384da0f892b6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 26 Nov 2025 17:51:41 +0100 Subject: [PATCH] t5563: verify that NTLM authentication works Although NTLM authentication is considered weak (extending even to NTLMv2, which purportedly allows brute-forcing reasonably complex 8-character passwords in a matter of days, given ample compute resources), it _is_ one of the authentication methods supported by libcurl. Note: The added test case *cannot* reuse the existing `custom_auth` facility. The reason is that that facility is backed by an NPH script ("No Parse Headers"), which does not allow handling the 3-phase NTLM authentication correctly (in my hands, the NPH script would not even be called upon the Type 3 message, a "200 OK" would be returned, but no headers, let alone the `git http-backend` output as payload). Having a separate NTLM authentication script makes the exact workings clearer and more readable, anyway. Co-authored-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- t/lib-httpd.sh | 1 + t/lib-httpd/apache.conf | 8 ++++ t/lib-httpd/ntlm-handshake.sh | 38 +++++++++++++++++++ t/t5563-simple-http-auth.sh | 71 +++-------------------------------- 4 files changed, 53 insertions(+), 65 deletions(-) create mode 100755 t/lib-httpd/ntlm-handshake.sh diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh index fc646447d5..68823c6ed2 100644 --- a/t/lib-httpd.sh +++ b/t/lib-httpd.sh @@ -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" diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index 664f23fc6c..192271ba99 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -155,6 +155,13 @@ SetEnv PERL_PATH ${PERL_PATH} CGIPassAuth on + + SetEnv GIT_EXEC_PATH ${GIT_EXEC_PATH} + SetEnv GIT_HTTP_EXPORT_ALL + + CGIPassAuth on + + 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 Options FollowSymlinks diff --git a/t/lib-httpd/ntlm-handshake.sh b/t/lib-httpd/ntlm-handshake.sh new file mode 100755 index 0000000000..3cf1266e40 --- /dev/null +++ b/t/lib-httpd/ntlm-handshake.sh @@ -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 diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 349ae4ab39..2082e553ae 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -764,78 +764,19 @@ test_expect_success 'access using three-legged auth' ' EOF ' -test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"' +test_lazy_prereq NTLM 'curl --version | grep -q NTLM' -test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' ' +test_expect_success NTLM 'access using NTLM auth' ' test_when_finished "per_test_cleanup" && set_credential_reply get <<-EOF && - username=alice - password=secret-passwd - EOF - - # Basic base64(alice:secret-passwd) - cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && - id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== - EOF - - cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && - id=1 status=200 - id=default response=WWW-Authenticate: Negotiate - id=default response=WWW-Authenticate: Basic realm="example.com" + username=user + password=pwd EOF test_config_global credential.helper test-helper && - GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \ - git -c http.emptyAuth=auto \ - ls-remote "$HTTPD_URL/custom_auth/repo.git" && - - # In auto mode with a Negotiate+Basic server, there should be - # three 401 responses: (1) initial no-auth request, (2) empty-auth - # retry where Negotiate fails (no Kerberos ticket), (3) libcurl - # internal Negotiate retry. The fourth attempt uses Basic - # credentials from credential_fill and succeeds. - grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s && - test_line_count = 3 actual_401s && - - expect_credential_query get <<-EOF - capability[]=authtype - capability[]=state - protocol=http - host=$HTTPD_DEST - wwwauth[]=Negotiate - wwwauth[]=Basic realm="example.com" - EOF -' - -test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' ' - test_when_finished "per_test_cleanup" && - - set_credential_reply get <<-EOF && - username=alice - password=secret-passwd - EOF - - # Basic base64(alice:secret-passwd) - cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && - id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== - EOF - - cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && - id=1 status=200 - id=default response=WWW-Authenticate: Negotiate - id=default response=WWW-Authenticate: Basic realm="example.com" - EOF - - test_config_global credential.helper test-helper && - GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \ - git -c http.emptyAuth=false \ - ls-remote "$HTTPD_URL/custom_auth/repo.git" && - - # With emptyAuth=false, Negotiate is stripped immediately and - # credential_fill is called right away. Only one 401 response. - grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s && - test_line_count = 1 actual_401s + GIT_TRACE_CURL=1 \ + git ls-remote "$HTTPD_URL/ntlm_auth/repo.git" ' test_done