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 <mjcheetham@outlook.com>
This commit is contained in:
Matthew John Cheetham 2026-04-14 13:52:28 +01:00
parent 25a742e6e4
commit e16ac344de
8 changed files with 95 additions and 7 deletions

View file

@ -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()

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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)

View file

@ -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;

View file

@ -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;