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

@ -1344,6 +1344,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

@ -2097,6 +2097,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

@ -517,6 +517,7 @@ the parent project, ideally in the "extra" find package redirect file:
Available variables:
- `HAVE_DES_ECB_ENCRYPT`: `DES_ecb_encrypt` present in OpenSSL (or fork).
- `HAVE_GSS_SET_NEG_MECHS`: `gss_set_neg_mechs` present in GSS-API library.
- `HAVE_LDAP_INIT_FD`: `ldap_init_fd` present in LDAP library.
- `HAVE_LDAP_URL_PARSE`: `ldap_url_parse` present in LDAP library.
- `HAVE_MBEDTLS_DES_CRYPT_ECB`: `mbedtls_des_crypt_ecb` present in mbedTLS <4.

View file

@ -330,6 +330,9 @@
/* if you have the GNU gssapi libraries */
#cmakedefine HAVE_GSSGNU 1
/* if you have gss_set_neg_mechs */
#cmakedefine HAVE_GSS_SET_NEG_MECHS 1
/* MIT Kerberos version */
#cmakedefine CURL_KRB5_VERSION ${CURL_KRB5_VERSION}

View file

@ -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()
*/
@ -93,6 +92,12 @@ struct stub_gss_ctx_id_t_desc {
char creds[250];
};
/* Stub credential: tracks which mechanisms are allowed */
struct stub_gss_cred_id_t_desc {
int allow_krb5;
int allow_ntlm;
};
static OM_uint32 stub_gss_init_sec_context(
OM_uint32 *min,
gss_cred_id_t initiator_cred_handle,
@ -116,7 +121,6 @@ static OM_uint32 stub_gss_init_sec_context(
char *token = NULL;
const char *creds = NULL;
(void)initiator_cred_handle;
(void)mech_type;
(void)time_req;
(void)input_chan_bindings;
@ -214,6 +218,16 @@ static OM_uint32 stub_gss_init_sec_context(
if(strstr(creds, "NTLM"))
ctx->have_ntlm = 1;
/* If a credential restricts allowed mechs, honour it */
if(initiator_cred_handle != GSS_C_NO_CREDENTIAL) {
struct stub_gss_cred_id_t_desc *cred =
(struct stub_gss_cred_id_t_desc *)initiator_cred_handle;
if(!cred->allow_krb5)
ctx->have_krb5 = 0;
if(!cred->allow_ntlm)
ctx->have_ntlm = 0;
}
if(ctx->have_krb5)
ctx->sent = STUB_GSS_KRB5;
else if(ctx->have_ntlm)
@ -307,6 +321,174 @@ static OM_uint32 stub_gss_delete_sec_context(
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) {
switch(context->sent) {
case STUB_GSS_NTLM1:
case STUB_GSS_NTLM3:
*mech_type = &stub_ntlmssp_oid;
break;
default:
*mech_type = (gss_OID)&Curl_krb5_mech_oid;
break;
}
}
return GSS_S_COMPLETE;
}
static OM_uint32 stub_gss_acquire_cred(
OM_uint32 *min,
gss_name_t desired_name,
OM_uint32 time_req,
gss_OID_set desired_mechs,
gss_cred_usage_t cred_usage,
gss_cred_id_t *output_cred_handle,
gss_OID_set *actual_mechs,
OM_uint32 *time_rec)
{
(void)desired_name;
(void)time_req;
(void)desired_mechs;
(void)cred_usage;
(void)actual_mechs;
(void)time_rec;
if(!min)
return GSS_S_FAILURE;
*min = 0;
/* Allocate a stub credential that initially allows all mechanisms */
if(output_cred_handle) {
struct stub_gss_cred_id_t_desc *cred =
curlx_calloc(1, sizeof(*cred));
if(!cred) {
*min = STUB_GSS_NO_MEMORY;
return GSS_S_FAILURE;
}
cred->allow_krb5 = 1;
cred->allow_ntlm = 1;
*output_cred_handle = (gss_cred_id_t)cred;
}
return GSS_S_COMPLETE;
}
static OM_uint32 stub_gss_indicate_mechs(
OM_uint32 *min,
gss_OID_set *mech_set)
{
const char *creds;
OM_uint32 major;
if(!min)
return GSS_S_FAILURE;
*min = 0;
creds = getenv("CURL_STUB_GSS_CREDS");
if(!creds) {
*min = STUB_GSS_INVALID_CREDS;
return GSS_S_FAILURE;
}
major = gss_create_empty_oid_set(min, mech_set);
if(GSS_ERROR(major))
return major;
/* Always include Kerberos */
gss_add_oid_set_member(min, (gss_OID)&Curl_krb5_mech_oid, mech_set);
/* Include NTLM if the stub creds contain NTLM */
if(strstr(creds, "NTLM"))
gss_add_oid_set_member(min, &stub_ntlmssp_oid, mech_set);
return GSS_S_COMPLETE;
}
#ifdef HAVE_GSS_SET_NEG_MECHS
static OM_uint32 stub_gss_set_neg_mechs(
OM_uint32 *min,
gss_cred_id_t cred_handle,
const gss_OID_set mech_set)
{
struct stub_gss_cred_id_t_desc *cred;
size_t i;
int found_krb5 = 0;
int found_ntlm = 0;
if(!min)
return GSS_S_FAILURE;
*min = 0;
if(cred_handle == GSS_C_NO_CREDENTIAL)
return GSS_S_FAILURE;
cred = (struct stub_gss_cred_id_t_desc *)cred_handle;
/* Determine which mechs are in the allowed set */
if(mech_set) {
for(i = 0; i < mech_set->count; i++) {
gss_OID oid = &mech_set->elements[i];
if(oid->length == Curl_krb5_mech_oid.length &&
!memcmp(oid->elements, Curl_krb5_mech_oid.elements, oid->length))
found_krb5 = 1;
if(oid->length == stub_ntlmssp_oid.length &&
!memcmp(oid->elements, stub_ntlmssp_oid.elements, oid->length))
found_ntlm = 1;
}
}
cred->allow_krb5 = found_krb5;
cred->allow_ntlm = found_ntlm;
return GSS_S_COMPLETE;
}
#endif /* HAVE_GSS_SET_NEG_MECHS */
static OM_uint32 stub_gss_release_cred(
OM_uint32 *min,
gss_cred_id_t *cred_handle)
{
if(!min)
return GSS_S_FAILURE;
*min = 0;
if(cred_handle && *cred_handle != GSS_C_NO_CREDENTIAL) {
curlx_free(*cred_handle);
*cred_handle = GSS_C_NO_CREDENTIAL;
}
return GSS_S_COMPLETE;
}
#endif /* CURL_GSS_STUB */
OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data,
@ -318,7 +500,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;
@ -340,7 +523,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,
@ -355,7 +538,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,
@ -383,6 +566,80 @@ 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);
}
OM_uint32 Curl_gss_acquire_cred(OM_uint32 *minor_status,
gss_name_t desired_name,
OM_uint32 time_req,
gss_OID_set desired_mechs,
gss_cred_usage_t cred_usage,
gss_cred_id_t *output_cred_handle,
gss_OID_set *actual_mechs,
OM_uint32 *time_rec)
{
#ifdef CURL_GSS_STUB
if(getenv("CURL_STUB_GSS_CREDS"))
return stub_gss_acquire_cred(minor_status, desired_name, time_req,
desired_mechs, cred_usage,
output_cred_handle, actual_mechs, time_rec);
#endif /* CURL_GSS_STUB */
return gss_acquire_cred(minor_status, desired_name, time_req,
desired_mechs, cred_usage,
output_cred_handle, actual_mechs, time_rec);
}
OM_uint32 Curl_gss_indicate_mechs(OM_uint32 *minor_status,
gss_OID_set *mech_set)
{
#ifdef CURL_GSS_STUB
if(getenv("CURL_STUB_GSS_CREDS"))
return stub_gss_indicate_mechs(minor_status, mech_set);
#endif /* CURL_GSS_STUB */
return gss_indicate_mechs(minor_status, mech_set);
}
#ifdef HAVE_GSS_SET_NEG_MECHS
OM_uint32 Curl_gss_set_neg_mechs(OM_uint32 *minor_status,
gss_cred_id_t cred_handle,
const gss_OID_set mech_set)
{
#ifdef CURL_GSS_STUB
if(getenv("CURL_STUB_GSS_CREDS"))
return stub_gss_set_neg_mechs(minor_status, cred_handle, mech_set);
#endif /* CURL_GSS_STUB */
return gss_set_neg_mechs(minor_status, cred_handle, mech_set);
}
#endif /* HAVE_GSS_SET_NEG_MECHS */
OM_uint32 Curl_gss_release_cred(OM_uint32 *minor_status,
gss_cred_id_t *cred_handle)
{
#ifdef CURL_GSS_STUB
if(getenv("CURL_STUB_GSS_CREDS"))
return stub_gss_release_cred(minor_status, cred_handle);
#endif /* CURL_GSS_STUB */
return gss_release_cred(minor_status, cred_handle);
}
#ifdef CURLVERBOSE
#define GSS_LOG_BUFFER_LEN 1024
static size_t display_gss_error(OM_uint32 status, int type,

View file

@ -41,12 +41,38 @@ 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,
gss_buffer_t output_token);
OM_uint32 Curl_gss_inquire_context(OM_uint32 *minor_status,
gss_ctx_id_t context,
gss_OID *mech_type);
OM_uint32 Curl_gss_acquire_cred(OM_uint32 *minor_status,
gss_name_t desired_name,
OM_uint32 time_req,
gss_OID_set desired_mechs,
gss_cred_usage_t cred_usage,
gss_cred_id_t *output_cred_handle,
gss_OID_set *actual_mechs,
OM_uint32 *time_rec);
OM_uint32 Curl_gss_indicate_mechs(OM_uint32 *minor_status,
gss_OID_set *mech_set);
#ifdef HAVE_GSS_SET_NEG_MECHS
OM_uint32 Curl_gss_set_neg_mechs(OM_uint32 *minor_status,
gss_cred_id_t cred_handle,
const gss_OID_set mech_set);
#endif
OM_uint32 Curl_gss_release_cred(OM_uint32 *minor_status,
gss_cred_id_t *cred_handle);
#ifdef CURLVERBOSE
/* Helper to log a GSS-API error status */
void Curl_gss_log_error(struct Curl_easy *data, const char *prefix,

View file

@ -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) {
curlx_safefree(identity->User);

View file

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

View file

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

View file

@ -176,7 +176,8 @@ static CURLcode socks5_gss_auth_loop(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) {
curlx_safefree(gss_recv_token.value);

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;

View file

@ -252,7 +252,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 test2092 test2093 \
test2088 test2089 test2090 test2091 test2092 test2093 test2094 \
test2100 test2101 test2102 test2103 test2104 test2105 test2106 test2107 \
test2108 test2109 test2110 test2113 test2114 test2115 test2116 test2117 \
\

View file

@ -5,45 +5,17 @@
HTTP
HTTP GET
HTTP Negotiate auth (stub ntlm)
SPNEGO NTLM disallowed
</keywords>
</info>
# Server-side
<reply>
<!-- First request, expect 401 (ntlm challenge) -->
<data1>
HTTP/1.1 401 Authorization Required
Server: Microsoft-IIS/7.0
Content-Type: text/html; charset=iso-8859-1
WWW-Authenticate: Negotiate Qw==
Content-Length: 19
<data nocheck="yes" crlf="headers">
HTTP/1.1 200 OK swsclose
Content-Length: 23
Still not yet sir!
</data1>
<!-- Second request, expect success -->
<data2>
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!
</data2>
<datacheck>
HTTP/1.1 401 Authorization Required
Server: Microsoft-IIS/7.0
Content-Type: text/html; charset=iso-8859-1
WWW-Authenticate: Negotiate Qw==
Content-Length: 19
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!
</datacheck>
This IS the real page!
</data>
</reply>
# Client-side
@ -52,7 +24,7 @@ Nice auth sir!
http
</server>
<name>
HTTP Negotiate authentication (stub NTLM)
HTTP Negotiate authentication blocked for stub NTLM credentials
</name>
<features>
GSS-API
@ -68,16 +40,15 @@ CURL_STUB_GSS_CREDS="NTLM_Alice"
# Verify data after the test has been "shot"
<verify>
<errorcode>
0
</errorcode>
# NTLM is blocked within SPNEGO, so when only NTLM credentials are
# available, negotiate auth silently fails and the request is sent
# without any Authorization header.
<protocol crlf="headers">
GET /%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
Authorization: Negotiate %b64["NTLM_Alice":HTTP@127.0.0.1:2:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]b64%
User-Agent: curl/%VERSION
Accept: */*
GET /%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
Authorization: Negotiate %b64["NTLM_Alice":HTTP@127.0.0.1:3:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]b64%
User-Agent: curl/%VERSION
Accept: */*

68
tests/data/test2094 Normal file
View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP GET
HTTP Negotiate auth (stub krb5)
SPNEGO NTLM disallowed
</keywords>
</info>
# Server-side
<reply>
<data1>
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!
</data1>
<datacheck>
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!
</datacheck>
</reply>
# Client-side
<client>
<server>
http
</server>
<name>
SPNEGO with Kerberos still works when NTLM is blocked
</name>
<features>
GSS-API
Debug
</features>
<setenv>
CURL_STUB_GSS_CREDS="KRB5_Alice"
</setenv>
<command>
--negotiate http://%HOSTIP:%HTTPPORT/%TESTNUMBER
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
0
</errorcode>
<protocol crlf="headers">
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: */*
</protocol>
</verify>
</testcase>

View file

@ -128,3 +128,14 @@
fun:inet_pton6
fun:ipv6_parse
}
{
mit-krb5-gss_display_status-internal-leak
Memcheck:Leak
match-leak-kinds: definite
fun:realloc
...
fun:gss_display_status
fun:display_gss_error
fun:Curl_gss_log_error
}