]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
spnego: block NTLM fallback in SPNEGO negotiation
authorMatthew John Cheetham <mjcheetham@outlook.com>
Mon, 13 Apr 2026 11:58:52 +0000 (12:58 +0100)
committerJay Satiro <raysatiro@yahoo.com>
Mon, 27 Jul 2026 21:28:52 +0000 (17:28 -0400)
- 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

19 files changed:
CMakeLists.txt
configure.ac
docs/INSTALL-CMAKE.md
lib/curl_config-cmake.h.in
lib/curl_gssapi.c
lib/curl_gssapi.h
lib/curl_sspi.c
lib/curl_sspi.h
lib/ldap.c
lib/socks_gssapi.c
lib/vauth/digest_sspi.c
lib/vauth/krb5_gssapi.c
lib/vauth/spnego_gssapi.c
lib/vauth/spnego_sspi.c
lib/vauth/vauth.h
tests/data/Makefile.am
tests/data/test2057
tests/data/test2094 [new file with mode: 0644]
tests/valgrind.supp

index 663ca5408157c65516e65fd23941f97a57b091c9..dcb735d0ce66c857d9b7b83e42cde6cb9d55755e 100644 (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()
index 0d2ee2b3ff32ad30c4fee576aaba3df9c10aa31b..c40bfb2a1cac215f1cac40ab45986c149d34d08a 100644 (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
index d241efb0077bfd78cefc68a7fc27390e606eb39a..f5af80a418b298bde87bbd1c583c5cd371b5ff1f 100644 (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.
index ffcce8273c8e3eb8ea648350a05ceeeea813a485..c992434530ee5ed1e72637142fe6d6e22ffdaec3 100644 (file)
 /* 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}
 
index fbc3c262f16643be24789c3d51444aebf1acde64..ccc24e09b1281332301ae5a4f49f2f98a9370a3a 100644 (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,
index fc3759ebcbcf2346a6589318291f256a438b754a..b33df77c17ae0ce87968b12169fd00b1f1160c4a 100644 (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,
index b7dcb7d55cc213eae6de8831a9fb763fc04d48e2..1429f7143a2e46aa844430845c1264e477845615 100644 (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);
index 2bd7eb4be88e83e89177ed484346ed42f776c5f4..992e01a9f25558ca353a8e0719d86ebb8f5d09f1 100644 (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;
index 63a40784a807edb585fb9ca96389a6be8c8d2387..dac472d4c9869e7a1351488cbd881f2012f9c40c 100644 (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));
index 9a5ad62f5c5ec4093bc77706d8f72a7528d15395..c1cfba5b71892154b35187bdbb78e5b4b37d5dd3 100644 (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);
index 305e367e175b30f2e5cac999f82b79c9c26ded89..c9b1432830a13014eef60eb68537bfa62c4f01d8 100644 (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;
index 41c47c055d3bad47a02754251c6dcfa93d92599e..6d9a125177430e169b38e018d91f56ccbd40d543 100644 (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)
index 1483b1f1e5b690e8cd837ee7b2e823865d62f861..b2bc28fb3fa7c965af75b2e129a770cd5e27b28e 100644 (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;
index b7d82c04ddfb18d8659ef1a973681ed9ab308a54..da9d7b023b952052a74d09d13fbe235375e0b8a0 100644 (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)
index 0f82f929453d5e1567351b336faaa887e757ff9a..f8ec563e92bbe367ab5827a7c21b259110a99418 100644 (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;
index 728858dafa2d443bae4a948585b9f9b4bedcb234..76ac5be1bf2631e66978d09346ee5cee859326c0 100644 (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 \
 \
index 18ce33d947a403bbe50b61bcdce60ba72144a2f7..d2ca3b79682a76957c2ba531a8e10a4b306b57bb 100644 (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: */*
 
diff --git a/tests/data/test2094 b/tests/data/test2094
new file mode 100644 (file)
index 0000000..57ded44
--- /dev/null
@@ -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>
index aad7255248839c5219876cf1ce7bdb5ea8706f85..026723d5d19a3a27d5ed0026967804c5f147ef64 100644 (file)
    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
+}