spnego: block NTLM fallback in SPNEGO negotiation

- Switch the Windows SSPI identity struct to SEC_WINNT_AUTH_IDENTITY_EX
  to use !ntlm in PackageList to prevent NTLM from being offered.

- For GSS filter out NTLMSSP OID, and restrict via gss_set_neg_mechs()
  to prevent NTLM from being offered.

- Extend the GSS-API debug stub layer to support the NTLM blocking logic
  without a real Kerberos environment.

- Update test 2057 to check that negotiate auth is silently skipped with
  no Authorization header when only NTLM stub credentials are available.

- Add SPNEGO NTLM blocking test 2093 which verifies that Kerberos
  credentials still succeed when NTLM is blocked within SPNEGO.

- Suppress tests valgrind leak for MIT krb5 gss_display_status, since
  the leak is in the library and not in curl.

To suppress the tests valgrind leak, the wildcard '...' bridges over an
anonymous frame inside libgssapi_krb5.so that valgrind reports as '???'.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Aided-by: Johannes Schindelin

Closes https://github.com/curl/curl/pull/21315
Closes https://github.com/curl/curl/pull/22410
This commit is contained in:
Matthew John Cheetham 2026-04-13 12:58:52 +01:00 committed by Jay Satiro
parent c9ead9bd1c
commit a8881e5e1d
19 changed files with 524 additions and 69 deletions

View file

@ -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;
@ -243,7 +243,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;
@ -465,8 +465,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;

View file

@ -136,7 +136,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)

View file

@ -158,6 +158,57 @@ 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 = Curl_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);
curlx_safefree(input_token.value);
return CURLE_AUTH_ERROR;
}
/* Get all available mechanisms */
major_status = Curl_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 */
major_status = Curl_gss_set_neg_mechs(&minor_status, nego->cred,
filtered_mechs);
if(GSS_ERROR(major_status)) {
Curl_gss_log_error(data, "gss_set_neg_mechs() failed: ",
major_status, minor_status);
}
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 +219,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 */
curlx_safefree(input_token.value);
@ -191,6 +243,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 = 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,
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 +355,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) {
Curl_gss_release_cred(&minor_status, &nego->cred);
nego->cred = GSS_C_NO_CREDENTIAL;
}
/* Reset any variables */
nego->status = 0;
nego->noauthpersist = FALSE;

View file

@ -148,6 +148,32 @@ 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
*/
#ifdef UNICODE
nego->identity.PackageList =
(unsigned short *)CURL_UNCONST(TEXT("!ntlm"));
#else
nego->identity.PackageList =
(unsigned char *)CURL_UNCONST(TEXT("!ntlm"));
#endif
nego->identity.PackageListLength = 5;
/* Allocate our credentials handle */
nego->credentials = curlx_calloc(1, sizeof(CredHandle));
if(!nego->credentials)

View file

@ -169,8 +169,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;
@ -236,8 +236,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
@ -291,6 +291,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;
@ -303,8 +304,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;