From 9ba4ddbc76a8fb6e54d7528c2abaa801aae982fa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 13 Apr 2026 12:58:52 +0100 Subject: [PATCH 1/6] auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX Replace SEC_WINNT_AUTH_IDENTITY with SEC_WINNT_AUTH_IDENTITY_EX across all SSPI authentication code. The extended structure adds Version, Length, and PackageList fields while remaining backwards compatible with all SSPI functions. Available since Windows XP. Curl_create_sspi_identity now sets the Version and Length fields when initializing the structure. Signed-off-by: Matthew John Cheetham --- lib/curl_sspi.c | 6 ++++-- lib/curl_sspi.h | 6 +++--- lib/ldap.c | 2 +- lib/vauth/digest_sspi.c | 10 +++++----- lib/vauth/vauth.h | 12 ++++++------ 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/curl_sspi.c b/lib/curl_sspi.c index 1d4cf925d6..018bfff28e 100644 --- a/lib/curl_sspi.c +++ b/lib/curl_sspi.c @@ -93,7 +93,7 @@ void Curl_sspi_global_cleanup(void) * Returns CURLE_OK on success. */ CURLcode Curl_create_sspi_identity(const char *userp, const char *passwdp, - SEC_WINNT_AUTH_IDENTITY *identity) + SEC_WINNT_AUTH_IDENTITY_EX *identity) { xcharp_u useranddomain; xcharp_u user, dup_user; @@ -105,6 +105,8 @@ CURLcode Curl_create_sspi_identity(const char *userp, const char *passwdp, /* Initialize the identity */ memset(identity, 0, sizeof(*identity)); + identity->Version = SEC_WINNT_AUTH_IDENTITY_VERSION; + identity->Length = sizeof(*identity); useranddomain.tchar_ptr = curlx_convert_UTF8_to_tchar(userp); if(!useranddomain.tchar_ptr) @@ -195,7 +197,7 @@ CURLcode Curl_create_sspi_identity(const char *userp, const char *passwdp, * * identity [in/out] - The identity structure. */ -void Curl_sspi_free_identity(SEC_WINNT_AUTH_IDENTITY *identity) +void Curl_sspi_free_identity(SEC_WINNT_AUTH_IDENTITY_EX *identity) { if(identity) { Curl_safefree(identity->User); diff --git a/lib/curl_sspi.h b/lib/curl_sspi.h index 3779d51753..ea405f53a7 100644 --- a/lib/curl_sspi.h +++ b/lib/curl_sspi.h @@ -34,14 +34,14 @@ void Curl_sspi_global_cleanup(void); /* This is used to populate the domain in an SSPI identity structure */ CURLcode Curl_override_sspi_http_realm(const char *chlg, - SEC_WINNT_AUTH_IDENTITY *identity); + SEC_WINNT_AUTH_IDENTITY_EX *identity); /* This is used to generate an SSPI identity structure */ CURLcode Curl_create_sspi_identity(const char *userp, const char *passwdp, - SEC_WINNT_AUTH_IDENTITY *identity); + SEC_WINNT_AUTH_IDENTITY_EX *identity); /* This is used to free an SSPI identity structure */ -void Curl_sspi_free_identity(SEC_WINNT_AUTH_IDENTITY *identity); +void Curl_sspi_free_identity(SEC_WINNT_AUTH_IDENTITY_EX *identity); /* Forward-declaration of global variables defined in curl_sspi.c */ extern PSecurityFunctionTable Curl_pSecFn; diff --git a/lib/ldap.c b/lib/ldap.c index e223078b03..59369a556d 100644 --- a/lib/ldap.c +++ b/lib/ldap.c @@ -157,7 +157,7 @@ static ULONG ldap_win_bind_auth(LDAP *server, const char *user, const char *passwd, unsigned long authflags) { ULONG method = 0; - SEC_WINNT_AUTH_IDENTITY cred; + SEC_WINNT_AUTH_IDENTITY_EX cred; ULONG rc = LDAP_AUTH_METHOD_NOT_SUPPORTED; memset(&cred, 0, sizeof(cred)); diff --git a/lib/vauth/digest_sspi.c b/lib/vauth/digest_sspi.c index f29e569cd1..5f4b5e2735 100644 --- a/lib/vauth/digest_sspi.c +++ b/lib/vauth/digest_sspi.c @@ -95,8 +95,8 @@ CURLcode Curl_auth_create_digest_md5_message(struct Curl_easy *data, CredHandle credentials; CtxtHandle context; PSecPkgInfo SecurityPackage; - SEC_WINNT_AUTH_IDENTITY identity; - SEC_WINNT_AUTH_IDENTITY *p_identity; + SEC_WINNT_AUTH_IDENTITY_EX identity; + SEC_WINNT_AUTH_IDENTITY_EX *p_identity; SecBuffer chlg_buf; SecBuffer resp_buf; SecBufferDesc chlg_desc; @@ -240,7 +240,7 @@ CURLcode Curl_auth_create_digest_md5_message(struct Curl_easy *data, * Returns CURLE_OK on success. */ CURLcode Curl_override_sspi_http_realm(const char *chlg, - SEC_WINNT_AUTH_IDENTITY *identity) + SEC_WINNT_AUTH_IDENTITY_EX *identity) { xcharp_u domain, dup_domain; @@ -466,8 +466,8 @@ CURLcode Curl_auth_create_digest_http_message(struct Curl_easy *data, if(!digest->http_context) { CredHandle credentials; - SEC_WINNT_AUTH_IDENTITY identity; - SEC_WINNT_AUTH_IDENTITY *p_identity; + SEC_WINNT_AUTH_IDENTITY_EX identity; + SEC_WINNT_AUTH_IDENTITY_EX *p_identity; SecBuffer resp_buf; SecBufferDesc resp_desc; unsigned long attrs; diff --git a/lib/vauth/vauth.h b/lib/vauth/vauth.h index 3e66c89cb5..10a02321e3 100644 --- a/lib/vauth/vauth.h +++ b/lib/vauth/vauth.h @@ -170,8 +170,8 @@ struct ntlmdata { #endif CredHandle *credentials; CtxtHandle *context; - SEC_WINNT_AUTH_IDENTITY identity; - SEC_WINNT_AUTH_IDENTITY *p_identity; + SEC_WINNT_AUTH_IDENTITY_EX identity; + SEC_WINNT_AUTH_IDENTITY_EX *p_identity; size_t token_max; BYTE *output_token; BYTE *input_token; @@ -241,8 +241,8 @@ struct kerberos5data { CredHandle *credentials; CtxtHandle *context; TCHAR *spn; - SEC_WINNT_AUTH_IDENTITY identity; - SEC_WINNT_AUTH_IDENTITY *p_identity; + SEC_WINNT_AUTH_IDENTITY_EX identity; + SEC_WINNT_AUTH_IDENTITY_EX *p_identity; size_t token_max; BYTE *output_token; #else @@ -309,8 +309,8 @@ struct negotiatedata { SECURITY_STATUS status; CredHandle *credentials; CtxtHandle *context; - SEC_WINNT_AUTH_IDENTITY identity; - SEC_WINNT_AUTH_IDENTITY *p_identity; + SEC_WINNT_AUTH_IDENTITY_EX identity; + SEC_WINNT_AUTH_IDENTITY_EX *p_identity; TCHAR *spn; size_t token_max; BYTE *output_token; From 25a742e6e417f789bd3d5b1cb406e3591a4026fc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 14 Apr 2026 13:51:23 +0100 Subject: [PATCH 2/6] spnego/sspi: block NTLM via PackageList exclusion Use the SEC_WINNT_AUTH_IDENTITY_EX PackageList field to pass '!ntlm' to the Negotiate SSP, preventing NTLM from being selected during SPNEGO negotiation on Windows. Signed-off-by: Matthew John Cheetham --- lib/vauth/spnego_sspi.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/vauth/spnego_sspi.c b/lib/vauth/spnego_sspi.c index 1f73123a0d..e0029ba04a 100644 --- a/lib/vauth/spnego_sspi.c +++ b/lib/vauth/spnego_sspi.c @@ -146,6 +146,27 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, /* Use the current Windows user */ nego->p_identity = NULL; + /* Exclude NTLM from SPNEGO negotiation via the PackageList field */ + if(!nego->p_identity) { + memset(&nego->identity, 0, sizeof(nego->identity)); + nego->identity.Version = SEC_WINNT_AUTH_IDENTITY_VERSION; + nego->identity.Length = sizeof(nego->identity); + nego->identity.Flags = +#ifdef UNICODE + SEC_WINNT_AUTH_IDENTITY_UNICODE; +#else + SEC_WINNT_AUTH_IDENTITY_ANSI; +#endif + nego->p_identity = &nego->identity; + } + + /* Use the special name "!ntlm" to prevent NTLM from being used: + * https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_winnt_auth_identity_exa + */ + nego->identity.PackageList = + (unsigned TCHAR *)CURL_UNCONST(TEXT("!ntlm")); + nego->identity.PackageListLength = 5; + /* Allocate our credentials handle */ nego->credentials = curlx_calloc(1, sizeof(CredHandle)); if(!nego->credentials) From e16ac344dea3ca95cb38d3d0041464dc134920e5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 14 Apr 2026 13:52:28 +0100 Subject: [PATCH 3/6] spnego/gss-api: block NTLM via gss_set_neg_mechs Add credential-based NTLM filtering for GSS-API SPNEGO. Acquire explicit credentials, enumerate available mechanisms, filter out the NTLMSSP OID, and apply via gss_set_neg_mechs(). Also verify the negotiated mechanism after context establishment and reject NTLM if disallowed. Pass a cred_handle through Curl_gss_init_sec_context so SPNEGO can use the restricted credentials. Probe for gss_set_neg_mechs() availability (HAVE_GSS_SET_NEG_MECHS) in configure and CMake. Signed-off-by: Matthew John Cheetham --- CMakeLists.txt | 5 +++ configure.ac | 1 + lib/curl_gssapi.c | 7 ++-- lib/curl_gssapi.h | 3 +- lib/socks_gssapi.c | 3 +- lib/vauth/krb5_gssapi.c | 3 +- lib/vauth/spnego_gssapi.c | 79 ++++++++++++++++++++++++++++++++++++++- lib/vauth/vauth.h | 1 + 8 files changed, 95 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1eb64a9cd2..2cc86d2a1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1308,6 +1308,11 @@ if(CURL_USE_GSSAPI) elseif(GSS_VERSION) # MIT set(CURL_KRB5_VERSION "\"${GSS_VERSION}\"") endif() + + cmake_push_check_state() + list(APPEND CMAKE_REQUIRED_LIBRARIES CURL::gss) + check_function_exists("gss_set_neg_mechs" HAVE_GSS_SET_NEG_MECHS) + cmake_pop_check_state() else() message(WARNING "GSSAPI has been requested, but no supporting libraries found. Skipping.") endif() diff --git a/configure.ac b/configure.ac index 6d2cb5583b..98535dada8 100644 --- a/configure.ac +++ b/configure.ac @@ -1997,6 +1997,7 @@ if test "$want_gss" = "yes"; then AC_MSG_RESULT([no]) AC_MSG_ERROR([--with-gssapi was specified, but a GSS-API library was not found.]) ]) + AC_CHECK_FUNCS([gss_set_neg_mechs]) fi build_libstubgss=no diff --git a/lib/curl_gssapi.c b/lib/curl_gssapi.c index c0eb1c4db1..84e2d68819 100644 --- a/lib/curl_gssapi.c +++ b/lib/curl_gssapi.c @@ -319,7 +319,8 @@ OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data, gss_buffer_t input_token, gss_buffer_t output_token, const bool mutual_auth, - OM_uint32 *ret_flags) + OM_uint32 *ret_flags, + gss_cred_id_t cred_handle) { OM_uint32 req_flags = GSS_C_REPLAY_FLAG; @@ -341,7 +342,7 @@ OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data, #ifdef CURL_GSS_STUB if(getenv("CURL_STUB_GSS_CREDS")) return stub_gss_init_sec_context(minor_status, - GSS_C_NO_CREDENTIAL, /* cred_handle */ + cred_handle, (struct stub_gss_ctx_id_t_desc **)context, target_name, mech_type, @@ -356,7 +357,7 @@ OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data, #endif /* CURL_GSS_STUB */ return gss_init_sec_context(minor_status, - GSS_C_NO_CREDENTIAL, /* cred_handle */ + cred_handle, context, target_name, mech_type, diff --git a/lib/curl_gssapi.h b/lib/curl_gssapi.h index 0d3609715a..82839b4087 100644 --- a/lib/curl_gssapi.h +++ b/lib/curl_gssapi.h @@ -41,7 +41,8 @@ OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data, gss_buffer_t input_token, gss_buffer_t output_token, const bool mutual_auth, - OM_uint32 *ret_flags); + OM_uint32 *ret_flags, + gss_cred_id_t cred_handle); OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min, gss_ctx_id_t *context_handle, diff --git a/lib/socks_gssapi.c b/lib/socks_gssapi.c index 962fcd05b2..506db8fbda 100644 --- a/lib/socks_gssapi.c +++ b/lib/socks_gssapi.c @@ -180,7 +180,8 @@ CURLcode Curl_SOCKS5_gssapi_negotiate(struct Curl_cfilter *cf, gss_token, &gss_send_token, TRUE, - &gss_ret_flags); + &gss_ret_flags, + GSS_C_NO_CREDENTIAL); if(gss_token != GSS_C_NO_BUFFER) { Curl_safefree(gss_recv_token.value); diff --git a/lib/vauth/krb5_gssapi.c b/lib/vauth/krb5_gssapi.c index d6af18f40b..324bbb075b 100644 --- a/lib/vauth/krb5_gssapi.c +++ b/lib/vauth/krb5_gssapi.c @@ -138,7 +138,8 @@ CURLcode Curl_auth_create_gssapi_user_message(struct Curl_easy *data, &input_token, &output_token, mutual_auth, - NULL); + NULL, + GSS_C_NO_CREDENTIAL); if(GSS_ERROR(major_status)) { if(output_token.value) diff --git a/lib/vauth/spnego_gssapi.c b/lib/vauth/spnego_gssapi.c index af8498bad3..c129ddeee7 100644 --- a/lib/vauth/spnego_gssapi.c +++ b/lib/vauth/spnego_gssapi.c @@ -37,6 +37,7 @@ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif + /* * Curl_auth_is_spnego_supported() * @@ -158,6 +159,52 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, } #endif +#ifdef HAVE_GSS_SET_NEG_MECHS + /* Acquire explicit credentials and restrict SPNEGO sub-mechanisms to + * exclude NTLM. We enumerate all available mechanisms and filter out + * the NTLMSSP OID, matching SSPI's "!ntlm". */ + if(nego->cred == GSS_C_NO_CREDENTIAL) { + /* OID 1.3.6.1.4.1.311.2.2.10 (NTLMSSP) */ + static const gss_OID_desc ntlmssp_oid = { + 10, CURL_UNCONST("\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a") + }; + gss_OID_set available_mechs = GSS_C_NO_OID_SET; + gss_OID_set filtered_mechs = GSS_C_NO_OID_SET; + + /* Acquire default credentials for SPNEGO */ + major_status = gss_acquire_cred(&minor_status, GSS_C_NO_NAME, + GSS_C_INDEFINITE, GSS_C_NO_OID_SET, + GSS_C_INITIATE, &nego->cred, NULL, NULL); + if(GSS_ERROR(major_status)) { + Curl_gss_log_error(data, "gss_acquire_cred() failed: ", + major_status, minor_status); + Curl_safefree(input_token.value); + return CURLE_AUTH_ERROR; + } + + /* Get all available mechanisms */ + major_status = gss_indicate_mechs(&minor_status, &available_mechs); + if(!GSS_ERROR(major_status)) { + /* Build a set excluding NTLMSSP */ + major_status = gss_create_empty_oid_set(&minor_status, &filtered_mechs); + if(!GSS_ERROR(major_status)) { + size_t i; + for(i = 0; i < available_mechs->count; i++) { + gss_OID oid = &available_mechs->elements[i]; + if(oid->length != ntlmssp_oid.length || + memcmp(oid->elements, ntlmssp_oid.elements, oid->length)) { + gss_add_oid_set_member(&minor_status, oid, &filtered_mechs); + } + } + /* Restrict SPNEGO to only use non-NTLM mechanisms */ + gss_set_neg_mechs(&minor_status, nego->cred, filtered_mechs); + gss_release_oid_set(&minor_status, &filtered_mechs); + } + gss_release_oid_set(&minor_status, &available_mechs); + } + } +#endif /* HAVE_GSS_SET_NEG_MECHS */ + /* Generate our challenge-response message */ major_status = Curl_gss_init_sec_context(data, &minor_status, @@ -168,7 +215,8 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, &input_token, &output_token, TRUE, - NULL); + NULL, + nego->cred); /* Free the decoded challenge as it is not required anymore */ Curl_safefree(input_token.value); @@ -191,6 +239,29 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, return CURLE_AUTH_ERROR; } + /* Check if NTLM was selected and is disallowed */ + if(nego->context != GSS_C_NO_CONTEXT) { + /* OID 1.3.6.1.4.1.311.2.2.10 (NTLMSSP) */ + static const gss_OID_desc ntlmssp_oid = { + 10, CURL_UNCONST("\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a") + }; + OM_uint32 inquire_major, inquire_minor; + gss_OID mech_type = GSS_C_NO_OID; + + inquire_major = gss_inquire_context(&inquire_minor, nego->context, + NULL, NULL, NULL, &mech_type, + NULL, NULL, NULL); + if(!GSS_ERROR(inquire_major) && mech_type && + mech_type->length == ntlmssp_oid.length && + !memcmp(mech_type->elements, ntlmssp_oid.elements, + ntlmssp_oid.length)) { + infof(data, "SPNEGO chose NTLM, but NTLM is not allowed"); + gss_release_buffer(&unused_status, &output_token); + Curl_auth_cleanup_spnego(nego); + return CURLE_AUTH_ERROR; + } + } + /* Free previous token */ if(nego->output_token.length && nego->output_token.value) gss_release_buffer(&unused_status, &nego->output_token); @@ -280,6 +351,12 @@ void Curl_auth_cleanup_spnego(struct negotiatedata *nego) nego->spn = GSS_C_NO_NAME; } + /* Free our credentials */ + if(nego->cred != GSS_C_NO_CREDENTIAL) { + gss_release_cred(&minor_status, &nego->cred); + nego->cred = GSS_C_NO_CREDENTIAL; + } + /* Reset any variables */ nego->status = 0; nego->noauthpersist = FALSE; diff --git a/lib/vauth/vauth.h b/lib/vauth/vauth.h index 10a02321e3..31aeaed266 100644 --- a/lib/vauth/vauth.h +++ b/lib/vauth/vauth.h @@ -297,6 +297,7 @@ struct negotiatedata { OM_uint32 status; gss_ctx_id_t context; gss_name_t spn; + gss_cred_id_t cred; gss_buffer_desc output_token; #ifdef GSS_C_CHANNEL_BOUND_FLAG struct dynbuf channel_binding_data; From 8f213831783faaf6cdb08aea746780da6b7c62f7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 23 Mar 2026 14:10:46 +0000 Subject: [PATCH 4/6] gss-api: stub gss_inquire_context for debug builds The GSS-API debug stub did not implement gss_inquire_context, so the NTLM-detection logic in spnego_gssapi.c could not be exercised without a real Kerberos environment. Add stub_gss_inquire_context that returns the NTLMSSP OID when the stub context is in NTLM mode and the Kerberos OID otherwise. Wrap it behind Curl_gss_inquire_context so the stub is transparently selected when CURL_STUB_GSS_CREDS is set. Signed-off-by: Matthew John Cheetham --- lib/curl_gssapi.c | 60 ++++++++++++++++++++++++++++++++++++++- lib/curl_gssapi.h | 4 +++ lib/vauth/spnego_gssapi.c | 6 ++-- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/lib/curl_gssapi.c b/lib/curl_gssapi.c index 84e2d68819..f6b593653d 100644 --- a/lib/curl_gssapi.c +++ b/lib/curl_gssapi.c @@ -81,7 +81,6 @@ enum min_err_code { /* libcurl is also passing this struct to these functions, which are not yet * stubbed: - * gss_inquire_context() * gss_unwrap() * gss_wrap() */ @@ -308,6 +307,48 @@ stub_gss_delete_sec_context(OM_uint32 *min, return GSS_S_COMPLETE; } + +/* NTLMSSP OID: 1.3.6.1.4.1.311.2.2.10 */ +static gss_OID_desc stub_ntlmssp_oid = { + 10, CURL_UNCONST("\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a") +}; + +static OM_uint32 +stub_gss_inquire_context(OM_uint32 *min, + struct stub_gss_ctx_id_t_desc *context, + gss_name_t *src_name, + gss_name_t *targ_name, + OM_uint32 *lifetime_rec, + gss_OID *mech_type, + OM_uint32 *ctx_flags, + int *locally_initiated, + int *open_context) +{ + (void)src_name; + (void)targ_name; + (void)lifetime_rec; + (void)ctx_flags; + (void)locally_initiated; + (void)open_context; + + if(!min) + return GSS_S_FAILURE; + + if(!context) { + *min = STUB_GSS_INVALID_CTX; + return GSS_S_FAILURE; + } + + *min = 0; + if(mech_type) { + if(context->have_ntlm && !context->have_krb5) + *mech_type = &stub_ntlmssp_oid; + else + *mech_type = (gss_OID)&Curl_krb5_mech_oid; + } + + return GSS_S_COMPLETE; +} #endif /* CURL_GSS_STUB */ OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data, @@ -385,6 +426,23 @@ OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min, return gss_delete_sec_context(min, context, output_token); } +OM_uint32 Curl_gss_inquire_context(OM_uint32 *minor_status, + gss_ctx_id_t context, + gss_OID *mech_type) +{ +#ifdef CURL_GSS_STUB + if(getenv("CURL_STUB_GSS_CREDS")) + return stub_gss_inquire_context(minor_status, + (struct stub_gss_ctx_id_t_desc *)context, + NULL, NULL, NULL, mech_type, + NULL, NULL, NULL); +#endif /* CURL_GSS_STUB */ + + return gss_inquire_context(minor_status, context, + NULL, NULL, NULL, mech_type, + NULL, NULL, NULL); +} + #ifdef CURLVERBOSE #define GSS_LOG_BUFFER_LEN 1024 static size_t display_gss_error(OM_uint32 status, int type, diff --git a/lib/curl_gssapi.h b/lib/curl_gssapi.h index 82839b4087..1c8c00b1ee 100644 --- a/lib/curl_gssapi.h +++ b/lib/curl_gssapi.h @@ -48,6 +48,10 @@ OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min, gss_ctx_id_t *context_handle, gss_buffer_t output_token); +OM_uint32 Curl_gss_inquire_context(OM_uint32 *minor_status, + gss_ctx_id_t context, + gss_OID *mech_type); + #ifdef CURLVERBOSE /* Helper to log a GSS-API error status */ void Curl_gss_log_error(struct Curl_easy *data, const char *prefix, diff --git a/lib/vauth/spnego_gssapi.c b/lib/vauth/spnego_gssapi.c index c129ddeee7..5519ae4c85 100644 --- a/lib/vauth/spnego_gssapi.c +++ b/lib/vauth/spnego_gssapi.c @@ -248,9 +248,9 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, OM_uint32 inquire_major, inquire_minor; gss_OID mech_type = GSS_C_NO_OID; - inquire_major = gss_inquire_context(&inquire_minor, nego->context, - NULL, NULL, NULL, &mech_type, - NULL, NULL, NULL); + inquire_major = Curl_gss_inquire_context(&inquire_minor, + nego->context, + &mech_type); if(!GSS_ERROR(inquire_major) && mech_type && mech_type->length == ntlmssp_oid.length && !memcmp(mech_type->elements, ntlmssp_oid.elements, From 3ea51e7a1fdd51bc07242d1acebf07debef0020f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Mon, 23 Mar 2026 14:11:01 +0000 Subject: [PATCH 5/6] tests: add SPNEGO NTLM blocking tests test2092 verifies that when only NTLM credentials are available and CURL_DISABLE_NEGOTIATE_NTLM is active, SPNEGO auth is silently skipped and the request is sent without an Authorization header. test2093 verifies that Kerberos credentials still succeed when built with CURL_DISABLE_NEGOTIATE_NTLM. Both tests require the negotiate-ntlm-disabled feature, which is reported by curl --version as "SPNEGO-no-NTLM" when the compile-time option is active. Signed-off-by: Matthew John Cheetham --- tests/data/Makefile.am | 2 +- tests/data/test2092 | 58 +++++++++++++++++++++++++++++++++++ tests/data/test2093 | 68 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/data/test2092 create mode 100644 tests/data/test2093 diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 53abf60901..a0539785b7 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -254,7 +254,7 @@ test2056 test2057 test2058 test2059 test2060 test2061 test2062 test2063 \ test2064 test2065 test2066 test2067 test2068 test2069 test2070 test2071 \ test2072 test2073 test2074 test2075 test2076 test2077 test2078 test2079 \ test2080 test2081 test2082 test2083 test2084 test2085 test2086 test2087 \ -test2088 test2089 test2090 test2091 \ +test2088 test2089 test2090 test2091 test2092 test2093 \ test2100 test2101 test2102 test2103 test2104 \ \ test2200 test2201 test2202 test2203 test2204 test2205 \ diff --git a/tests/data/test2092 b/tests/data/test2092 new file mode 100644 index 0000000000..3cfdd9cab4 --- /dev/null +++ b/tests/data/test2092 @@ -0,0 +1,58 @@ + + + + +HTTP +HTTP GET +HTTP Negotiate auth (stub ntlm) +SPNEGO NTLM disallowed + + + +# Server-side + + +HTTP/1.1 200 OK swsclose +Content-Length: 23 + +This IS the real page! + + + +# Client-side + + +http + + +SPNEGO skips auth when NTLM blocked by CURL_DISABLE_NEGOTIATE_NTLM + + +GSS-API +Debug + + +CURL_STUB_GSS_CREDS="NTLM_Alice" + + +--negotiate http://%HOSTIP:%HTTPPORT/%TESTNUMBER + + + +# Verify data after the test has been "shot" + + +0 + +# When NTLM is the only available mechanism and is blocked, +# negotiate auth silently fails and the request is sent without +# any Authorization header. + +GET /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Accept: */* + + + + diff --git a/tests/data/test2093 b/tests/data/test2093 new file mode 100644 index 0000000000..b74979f384 --- /dev/null +++ b/tests/data/test2093 @@ -0,0 +1,68 @@ + + + + +HTTP +HTTP GET +HTTP Negotiate auth (stub krb5) +SPNEGO NTLM disallowed + + + +# Server-side + + +HTTP/1.1 200 Things are fine in server land +Server: Microsoft-IIS/7.0 +Content-Type: text/html; charset=iso-8859-1 +WWW-Authenticate: Negotiate RA== +Content-Length: 15 + +Nice auth sir! + + +HTTP/1.1 200 Things are fine in server land +Server: Microsoft-IIS/7.0 +Content-Type: text/html; charset=iso-8859-1 +WWW-Authenticate: Negotiate RA== +Content-Length: 15 + +Nice auth sir! + + + +# Client-side + + +http + + +SPNEGO with Kerberos still works when built with CURL_DISABLE_NEGOTIATE_NTLM + + +GSS-API +Debug + + +CURL_STUB_GSS_CREDS="KRB5_Alice" + + +--negotiate http://%HOSTIP:%HTTPPORT/%TESTNUMBER + + + +# Verify data after the test has been "shot" + + +0 + + +GET /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +Authorization: Negotiate %b64["KRB5_Alice":HTTP@127.0.0.1:1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]b64% +User-Agent: curl/%VERSION +Accept: */* + + + + From 0267a63dc04e3972d64d51f6e288b22f4ecf2756 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 14 Apr 2026 14:11:00 +0100 Subject: [PATCH 6/6] spnego: add --disable-negotiate-ntlm compile-time option Add configure and CMake options to define CURL_DISABLE_NEGOTIATE_NTLM, which gates the NTLM blocking logic in the SSPI and GSS-API SPNEGO code paths behind a compile-time flag. Add a 'SPNEGO-no-NTLM' feature string to curl --version output and gate the SPNEGO NTLM blocking tests on the negotiate-ntlm-disabled feature. Signed-off-by: Matthew John Cheetham --- CMakeLists.txt | 2 ++ configure.ac | 20 ++++++++++++++++++++ docs/CURL-DISABLE.md | 4 ++++ docs/libcurl/curl_version_info.md | 6 ++++++ lib/curl_config-cmake.h.in | 3 +++ lib/vauth/spnego_gssapi.c | 4 ++++ lib/vauth/spnego_sspi.c | 2 ++ lib/version.c | 3 +++ tests/data/test2092 | 1 + tests/data/test2093 | 1 + tests/runtests.pl | 2 ++ 11 files changed, 48 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2cc86d2a1f..6d7f77a3e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -426,6 +426,8 @@ option(CURL_DISABLE_KERBEROS_AUTH "Disable Kerberos authentication" OFF) mark_as_advanced(CURL_DISABLE_KERBEROS_AUTH) option(CURL_DISABLE_NEGOTIATE_AUTH "Disable negotiate authentication" OFF) mark_as_advanced(CURL_DISABLE_NEGOTIATE_AUTH) +option(CURL_DISABLE_NEGOTIATE_NTLM "Block NTLM within SPNEGO negotiation" OFF) +mark_as_advanced(CURL_DISABLE_NEGOTIATE_NTLM) option(CURL_DISABLE_AWS "Disable aws-sigv4" OFF) mark_as_advanced(CURL_DISABLE_AWS) option(CURL_DISABLE_DICT "Disable DICT" OFF) diff --git a/configure.ac b/configure.ac index 98535dada8..b9190f4166 100644 --- a/configure.ac +++ b/configure.ac @@ -4507,6 +4507,26 @@ AS_HELP_STRING([--disable-negotiate-auth],[Disable negotiate authentication]), AC_MSG_RESULT(yes) ) +dnl ************************************************************ +dnl disable NTLM within SPNEGO negotiation +dnl +AC_MSG_CHECKING([whether to allow NTLM within SPNEGO]) +AC_ARG_ENABLE(negotiate-ntlm, +AS_HELP_STRING([--enable-negotiate-ntlm],[Allow NTLM within SPNEGO (default)]) +AS_HELP_STRING([--disable-negotiate-ntlm],[Block NTLM within SPNEGO]), +[ case "$enableval" in + no) + AC_MSG_RESULT(no) + AC_DEFINE(CURL_DISABLE_NEGOTIATE_NTLM, 1, [to block NTLM within SPNEGO]) + CURL_DISABLE_NEGOTIATE_NTLM=1 + ;; + *) + AC_MSG_RESULT(yes) + ;; + esac ], + AC_MSG_RESULT(yes) +) + dnl ************************************************************ dnl disable aws dnl diff --git a/docs/CURL-DISABLE.md b/docs/CURL-DISABLE.md index c266f0c0ad..f3664898af 100644 --- a/docs/CURL-DISABLE.md +++ b/docs/CURL-DISABLE.md @@ -38,6 +38,10 @@ Disable support for the Kerberos authentication methods. Disable support for the negotiate authentication methods. +## `CURL_DISABLE_NEGOTIATE_NTLM` + +Block NTLM authentication within SPNEGO negotiation. + ## `CURL_DISABLE_AWS` Disable **aws-sigv4** support. diff --git a/docs/libcurl/curl_version_info.md b/docs/libcurl/curl_version_info.md index 83c7cdb9fe..8a389fa0c6 100644 --- a/docs/libcurl/curl_version_info.md +++ b/docs/libcurl/curl_version_info.md @@ -320,6 +320,12 @@ libcurl ignore cookies with a domain that is on the list. libcurl was built with support for SPNEGO authentication (Simple and Protected GSS-API Negotiation Mechanism, defined in RFC 2478.) +## `SPNEGO-no-NTLM` + +*features* mask bit: none + +NTLM authentication is blocked within SPNEGO negotiation. + ## `SSL` *features* mask bit: CURL_VERSION_SSL diff --git a/lib/curl_config-cmake.h.in b/lib/curl_config-cmake.h.in index 1dcab9d897..f5a16d2d2b 100644 --- a/lib/curl_config-cmake.h.in +++ b/lib/curl_config-cmake.h.in @@ -58,6 +58,9 @@ /* disables negotiate authentication */ #cmakedefine CURL_DISABLE_NEGOTIATE_AUTH 1 +/* blocks NTLM within SPNEGO negotiation */ +#cmakedefine CURL_DISABLE_NEGOTIATE_NTLM 1 + /* disables aws-sigv4 */ #cmakedefine CURL_DISABLE_AWS 1 diff --git a/lib/vauth/spnego_gssapi.c b/lib/vauth/spnego_gssapi.c index 5519ae4c85..1a1c7b212b 100644 --- a/lib/vauth/spnego_gssapi.c +++ b/lib/vauth/spnego_gssapi.c @@ -160,6 +160,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, #endif #ifdef HAVE_GSS_SET_NEG_MECHS +#ifdef CURL_DISABLE_NEGOTIATE_NTLM /* Acquire explicit credentials and restrict SPNEGO sub-mechanisms to * exclude NTLM. We enumerate all available mechanisms and filter out * the NTLMSSP OID, matching SSPI's "!ntlm". */ @@ -203,6 +204,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, gss_release_oid_set(&minor_status, &available_mechs); } } +#endif /* CURL_DISABLE_NEGOTIATE_NTLM */ #endif /* HAVE_GSS_SET_NEG_MECHS */ /* Generate our challenge-response message */ @@ -240,6 +242,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, } /* Check if NTLM was selected and is disallowed */ +#ifdef CURL_DISABLE_NEGOTIATE_NTLM if(nego->context != GSS_C_NO_CONTEXT) { /* OID 1.3.6.1.4.1.311.2.2.10 (NTLMSSP) */ static const gss_OID_desc ntlmssp_oid = { @@ -261,6 +264,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, return CURLE_AUTH_ERROR; } } +#endif /* CURL_DISABLE_NEGOTIATE_NTLM */ /* Free previous token */ if(nego->output_token.length && nego->output_token.value) diff --git a/lib/vauth/spnego_sspi.c b/lib/vauth/spnego_sspi.c index e0029ba04a..e07978fb64 100644 --- a/lib/vauth/spnego_sspi.c +++ b/lib/vauth/spnego_sspi.c @@ -146,6 +146,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, /* Use the current Windows user */ nego->p_identity = NULL; +#ifdef CURL_DISABLE_NEGOTIATE_NTLM /* Exclude NTLM from SPNEGO negotiation via the PackageList field */ if(!nego->p_identity) { memset(&nego->identity, 0, sizeof(nego->identity)); @@ -166,6 +167,7 @@ CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data, nego->identity.PackageList = (unsigned TCHAR *)CURL_UNCONST(TEXT("!ntlm")); nego->identity.PackageListLength = 5; +#endif /* CURL_DISABLE_NEGOTIATE_NTLM */ /* Allocate our credentials handle */ nego->credentials = curlx_calloc(1, sizeof(CredHandle)); diff --git a/lib/version.c b/lib/version.c index 7ccd875dc8..3976490163 100644 --- a/lib/version.c +++ b/lib/version.c @@ -525,6 +525,9 @@ static const struct feat features_table[] = { #endif /* USE_SSL */ #ifdef USE_SPNEGO FEATURE("SPNEGO", NULL, CURL_VERSION_SPNEGO), +#ifdef CURL_DISABLE_NEGOTIATE_NTLM + FEATURE("SPNEGO-no-NTLM", NULL, 0), +#endif #endif #ifdef USE_SSL FEATURE("SSL", NULL, CURL_VERSION_SSL), diff --git a/tests/data/test2092 b/tests/data/test2092 index 3cfdd9cab4..5423e15055 100644 --- a/tests/data/test2092 +++ b/tests/data/test2092 @@ -30,6 +30,7 @@ SPNEGO skips auth when NTLM blocked by CURL_DISABLE_NEGOTIATE_NTLM GSS-API Debug +negotiate-ntlm-disabled CURL_STUB_GSS_CREDS="NTLM_Alice" diff --git a/tests/data/test2093 b/tests/data/test2093 index b74979f384..95f84c2656 100644 --- a/tests/data/test2093 +++ b/tests/data/test2093 @@ -42,6 +42,7 @@ SPNEGO with Kerberos still works when built with CURL_DISABLE_NEGOTIATE_NTLM GSS-API Debug +negotiate-ntlm-disabled CURL_STUB_GSS_CREDS="KRB5_Alice" diff --git a/tests/runtests.pl b/tests/runtests.pl index 2a180bfeb4..e444a5e23a 100755 --- a/tests/runtests.pl +++ b/tests/runtests.pl @@ -690,6 +690,8 @@ sub checksystemfeatures { $feature{"Kerberos"} = $feat =~ /Kerberos/i; # SPNEGO enabled $feature{"SPNEGO"} = $feat =~ /SPNEGO/i; + # SPNEGO NTLM disabled (compile-time) + $feature{"negotiate-ntlm-disabled"} = $feat =~ /SPNEGO-no-NTLM/i; # TLS-SRP enabled $feature{"TLS-SRP"} = $feat =~ /TLS-SRP/i; # PSL enabled