Skip to content

spnego: block NTLM fallback in SPNEGO negotiation - #21315

Closed
mjcheetham wants to merge 6 commits into
curl:masterfrom
mjcheetham:spnego-no-ntlm-v3
Closed

spnego: block NTLM fallback in SPNEGO negotiation#21315
mjcheetham wants to merge 6 commits into
curl:masterfrom
mjcheetham:spnego-no-ntlm-v3

Conversation

@mjcheetham

@mjcheetham mjcheetham commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Best reviewed commit-by-commit.

Motivation

Even if an application omits CURLAUTH_NTLM from CURLOPT_HTTPAUTH to prevent bare NTLM authentication, NTLM can still be used under the hood when CURLAUTH_NEGOTIATE is enabled — SPNEGO may silently select NTLM as its negotiated sub-mechanism.

On Windows, SSPI's Negotiate SSP includes NTLM with no easy way to prevent it. On other platforms, plugins such as gss-ntlmssp can add NTLM support to MIT Kerberos's GSS-API, enabling the same NTLM-over-SPNEGO fallback.

This is a follow-up to #21076. Rather than introducing a runtime option, this PR unconditionally blocks NTLM within SPNEGO negotiation.

Approach

Commits 1–5 implement unconditional NTLM blocking — SPNEGO will never select NTLM, regardless of build configuration.

SSPI (Windows)

Uses the SEC_WINNT_AUTH_IDENTITY_EX PackageList field to pass !ntlm to the Negotiate SSP, preventing NTLM from being offered during negotiation.

GSS-API (Unix/macOS)

Acquires explicit credentials, enumerates available mechanisms, filters out the NTLMSSP OID (1.3.6.1.4.1.311.2.2.10), and restricts the credential via gss_set_neg_mechs(). As a belt-and-braces check (and also for GSS-API implementations that lack gss_set_neg_mechs()) also inspects the negotiated mechanism after context establishment via gss_inquire_context() and rejects NTLM if it was selected.

Commits

  1. auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX — replace SEC_WINNT_AUTH_IDENTITY with the extended structure across all SSPI auth code, adding the PackageList field needed by commit 2.
  2. spnego/sspi: block NTLM via PackageList exclusion — use the !ntlm package exclusion on Windows.
  3. spnego/gss-api: block NTLM via gss_set_neg_mechs — filter NTLM from GSS-API SPNEGO and verify the negotiated mechanism post-handshake, update test2057 to expect no more NTLM auth.
  4. gss-api: stub gss_inquire_context for debug builds — extend the existing GSS-API stub layer so the NTLM detection logic can be tested without a real Kerberos environment.
  5. tests: add SPNEGO NTLM blocking tests — test2092 (Kerberos creds → still works when NTLM is blocked).

@mjcheetham

mjcheetham commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

Failing CI checks seem to be unrelated to these changes - flakes or also seen on other PRs and in commits on master:

  • Linux HTTP/3 / CM boringssl
    SOCKS4 proxy download timed out transferring 3×10MB files. Runner flake?

  • macOS / AM clang OpenSSL SecTrust
    test_05_09_handshake_eof race? Server closed before curl connected.

  • macOS / CM clang OpenSSL torture 3
    test 1675 (URL API unit test) failed under forced allocation failures. Timing?

  • non-native / NetBSD, CM clang openssl x86_64
    Package mirror timeout — build never started. Same failure on master.

  • Windows / mingw, CM clang-x86_64 gnutls libssh
    test 1501 (FTP slow LIST) — test server refused connections. Timing-dependent?

@mjcheetham
mjcheetham marked this pull request as ready for review April 14, 2026 14:52
@dscho

dscho commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

For the record (mainly for your benefit, @bagder 😉): Git for Windows already integrated the Windows-specific part of this, in preparation for moving all Git for Windows users off of NTLM until September.

@testclutch

Copy link
Copy Markdown

Analysis of PR #21315 at 0267a63d:

Test ../../tests/http/test_40_socks.py::TestSocks::test_40_03_dl_serial[http/1.1-socks4] failed, which has NOT been flaky recently, so there could be a real issue in this PR. Note that this test has failed in 2 different CI jobs (the link just goes to one of them).

Test ../../tests/http/test_05_errors.py::TestErrors::test_05_09_handshake_eof failed, but it has been 9.5% flaky lately, so it's probably NOT a fault of the PR. Note that this test has failed in 2 different CI jobs (the link just goes to one of them).

Test 1675 failed, which has NOT been flaky recently, so there could be a real issue in this PR. Note that this test has failed in 2 different CI jobs (the link just goes to one of them).

Test 1501 failed, which has NOT been flaky recently, so there could be a real issue in this PR. Note that this test has failed in 10 different CI jobs (the link just goes to one of them). Note that this CI job has had a number of other flaky tests recently (2, to be exact) so it may be that this failure is rather a systemic issue with this job and not with this specific PR.

Generated by Testclutch

Comment thread lib/vauth/spnego_gssapi.c Outdated
Comment thread lib/vauth/spnego_gssapi.c Outdated
Comment thread CMakeLists.txt
Comment thread CMakeLists.txt Outdated
Comment thread lib/curl_gssapi.c
Comment thread lib/curl_gssapi.c Outdated
Comment thread lib/version.c Outdated
@vszakats

vszakats commented Apr 14, 2026

Copy link
Copy Markdown
Member

May I ask, is this an LLM-generated patch and/or PR message?

@mjcheetham

Copy link
Copy Markdown
Contributor Author

May I ask, is this an LLM-generated patch and/or PR message?

I did make use of LLMs to help me explore the codebase (I'm a first time curl contributor) and find the correct places to modify the code, or insert flags for the compile-time defs - why I missed the INSTALL-CMAKE.md's feature detection variables section.

I also used Copilot to create a first draft of the PR description from the commits which I subsequently added and reworked myself. I checked any code/API completions from LLM-driven editor autocomplete.

@icing

icing commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Isn't it more the job of GSS and SSPI to refrain from using NTLM under the hood? Is there really value in curl detecting this and pull out the hammer?

@mjcheetham

mjcheetham commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Isn't it more the job of GSS and SSPI to refrain from using NTLM under the hood? Is there really value in curl detecting this and pull out the hammer?

This is a fair question. The issue is that today there is no way for applications using curl to actually use systems like SSPI to restrict the SSPs in use (e.g. not allowing NTLM).

SSPI provides in its API a way to filter the packages (SEC_WINNT_AUTH_IDENTITY_EX::PackageList). Applications would need to manually call SSPI and not use curl otherwise. Same applies for GSS-API via gss_set_neg_mechs.

From the user's point of view, the only control they would have is to modify their entire system to do something like disable NTLM everywhere (Group Policy docs) - these things require administrator permissions and are a system-wide hammer, so this isn't viable for most users.

Aside: if there's an issue with the idea of curl treating NTLM specifically as a bad package, I'd also be open to changing this to a more generic CURLOPT_NEGOTIATE_SSP_EXCLUDE to filter/exclude specific packages, for example.

@mjcheetham

mjcheetham commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed feedback from @vszakats. Fixups squashed; here's the range-diff:

Range-diff
  • 1: 9ba4ddb = 1: 9ba4ddb auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX
  • 2: 25a742e = 2: 25a742e spnego/sspi: block NTLM via PackageList exclusion
  • 3: e16ac34 ! 3: 8e5ce53 spnego/gss-api: block NTLM via gss_set_neg_mechs
        @@ configure.ac: if test "$want_gss" = "yes"; then
    
          build_libstubgss=no
    
        + ## docs/INSTALL-CMAKE.md ##
        +@@ docs/INSTALL-CMAKE.md: Available variables:
        +
        + - `HAVE_DES_ECB_ENCRYPT`:                   `DES_ecb_encrypt` present in OpenSSL (or fork).
        + - `HAVE_GNUTLS_SRP`:                        `gnutls_srp_verifier` present in GnuTLS.
        ++- `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.
        +
          ## lib/curl_gssapi.c ##
         @@ lib/curl_gssapi.c: OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data,
                                              gss_buffer_t input_token,
        @@ lib/vauth/krb5_gssapi.c: CURLcode Curl_auth_create_gssapi_user_message(struct Cu
              if(output_token.value)
    
          ## lib/vauth/spnego_gssapi.c ##
        -@@
        - #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
        - #endif
        -
        -+
        - /*
        -  * Curl_auth_is_spnego_supported()
        -  *
         @@ lib/vauth/spnego_gssapi.c: CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data,
            }
          #endif
  • 4: 8f21383 ! 4: e9bb2a6 gss-api: stub gss_inquire_context for debug builds
        @@ lib/curl_gssapi.c: stub_gss_delete_sec_context(OM_uint32 *min,
         +  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)
        ++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;
  • 5: 3ea51e7 = 5: 1a3f8b8 tests: add SPNEGO NTLM blocking tests
  • 6: 0267a63 ! 6: a8c82c8 spnego: add --disable-negotiate-ntlm compile-time option
        @@ lib/curl_config-cmake.h.in
    
          ## lib/vauth/spnego_gssapi.c ##
         @@ lib/vauth/spnego_gssapi.c: CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data,
        +   }
          #endif
    
        - #ifdef HAVE_GSS_SET_NEG_MECHS
        -+#ifdef CURL_DISABLE_NEGOTIATE_NTLM
        +-#ifdef HAVE_GSS_SET_NEG_MECHS
        ++#if defined(HAVE_GSS_SET_NEG_MECHS) && defined(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". */
        @@ lib/vauth/spnego_gssapi.c: CURLcode Curl_auth_decode_spnego_message(struct Curl_
                gss_release_oid_set(&minor_status, &available_mechs);
              }
            }
        -+#endif /* CURL_DISABLE_NEGOTIATE_NTLM */
        - #endif /* HAVE_GSS_SET_NEG_MECHS */
        +-#endif /* HAVE_GSS_SET_NEG_MECHS */
        ++#endif /* HAVE_GSS_SET_NEG_MECHS && CURL_DISABLE_NEGOTIATE_NTLM */
    
            /* Generate our challenge-response message */
        +   major_status = Curl_gss_init_sec_context(data,
         @@ lib/vauth/spnego_gssapi.c: CURLcode Curl_auth_decode_spnego_message(struct Curl_easy *data,
            }

@mjcheetham
mjcheetham requested review from dscho and vszakats April 20, 2026 13:18
Comment thread lib/version.c Outdated

@dscho dscho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finished reviewing the patches, and I think they are good. (I can't approve, of course, as I have no explicit access (read or otherwise) to this repository, otherwise I would approve).

Regarding the SPNEGO-no-NTLM feature: I sense that there is great reluctance to introduce this knob. I can see merits in both positions: A feature comes with a maintenance cost. On the other hand, users who currently rely on the functionality would most likely appreciate a smoother gliding path than upgrading and discovering that their previously-working setup broke without them feeling they had proper warning beforehand.

General point about this PR: In the interest of security, I would like to argue that NTLM via SPNEGO should be turned off by default, it is a big footgun for adminstrators who thought that they disabled NTLM only to realize that their Kerberos installation allowed downgrading to NTLM. Or worse: users who disabled NTLM, only to get bitten by that SPNEGO "backdoor".

If the consensus is not to introduce this SPNEGO-no-NTLM feature, I would like to register my desire on behalf of Git for Windows' users that the functionality of this feature be accepted and merged and "turned on by default", i.e. to disallow NTLM via SPNEGO always. That is, if this PR is not accepted in the current form, I would like to ask that at least the changes to lib/vauth/ would be accepted (and then no longer guarded behind a flag).

@bagder bagder added the feature-window A merge of this requires an open feature window label Apr 20, 2026
@bagder
bagder requested a review from Copilot April 20, 2026 21:00
@bagder

bagder commented Apr 20, 2026

Copy link
Copy Markdown
Member

I think we are ready to accept this PR without build option and without feature flag and just completely disable NTLM in SPNEGO. We just need to wait for the feature window to open again, which it should do on May 9 if nothing bad happens.

Then we can ship this in 8.21.0 in June and then drop the rest of the NTLM as planned in September for the release coming in October (probably 8.23.0).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens SPNEGO (Negotiate) authentication by preventing NTLM from being selected as a negotiated sub-mechanism, with a transitional compile-time switch (--disable-negotiate-ntlm / CURL_DISABLE_NEGOTIATE_NTLM) and accompanying feature/test plumbing.

Changes:

  • Add compile-time support to block NTLM within SPNEGO for both SSPI (Windows) and GSS-API (Unix/macOS), plus runtime feature reporting (SPNEGO-no-NTLM).
  • Extend internal GSS-API wrappers to support passing explicit credentials and to query the negotiated mechanism (including stub support for debug tests).
  • Add new integration tests (test2092/test2093) and wire them to a new test feature flag (negotiate-ntlm-disabled).

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/runtests.pl Detect new SPNEGO-no-NTLM feature and expose it as negotiate-ntlm-disabled for test gating.
tests/data/test2092 New test: NTLM-only stub creds + NTLM blocked ⇒ request proceeds without Authorization header.
tests/data/test2093 New test: Kerberos stub creds + NTLM blocked ⇒ Negotiate still succeeds and sends Authorization header.
tests/data/Makefile.am Register the new test cases in the test suite manifest.
lib/version.c Add SPNEGO-no-NTLM to the feature name list when CURL_DISABLE_NEGOTIATE_NTLM is enabled.
lib/vauth/vauth.h Add SPNEGO GSS-API credential handle storage; switch SSPI identity types to _EX.
lib/vauth/spnego_sspi.c For SSPI, exclude NTLM via SEC_WINNT_AUTH_IDENTITY_EX.PackageList="!ntlm" under the compile-time gate.
lib/vauth/spnego_gssapi.c For GSS-API, optionally restrict negotiated mechs via gss_set_neg_mechs() and always reject negotiated NTLM via context inquiry.
lib/vauth/krb5_gssapi.c Update Curl_gss_init_sec_context() call sites for the new credential-handle parameter.
lib/vauth/digest_sspi.c Switch SSPI identity usage to SEC_WINNT_AUTH_IDENTITY_EX.
lib/socks_gssapi.c Update Curl_gss_init_sec_context() call site for the new credential-handle parameter.
lib/ldap.c Switch LDAP SSPI identity struct to SEC_WINNT_AUTH_IDENTITY_EX.
lib/curl_sspi.h Update SSPI helper function signatures to use SEC_WINNT_AUTH_IDENTITY_EX.
lib/curl_sspi.c Initialize SEC_WINNT_AUTH_IDENTITY_EX with Version/Length; update free helper signature.
lib/curl_gssapi.h Extend Curl_gss_init_sec_context() signature; add Curl_gss_inquire_context() wrapper declaration.
lib/curl_gssapi.c Implement new Curl_gss_init_sec_context() signature and add a stub-capable Curl_gss_inquire_context() wrapper.
lib/curl_config-cmake.h.in Add CURL_DISABLE_NEGOTIATE_NTLM CMake-configurable define (plus related review note).
docs/libcurl/curl_version_info.md Document the new SPNEGO-no-NTLM feature string.
docs/INSTALL-CMAKE.md Document HAVE_GSS_SET_NEG_MECHS as a CMake-detected capability.
docs/CURL-DISABLE.md Document CURL_DISABLE_NEGOTIATE_NTLM.
configure.ac Add gss_set_neg_mechs detection and --disable-negotiate-ntlm configuration option.
CMakeLists.txt Add CURL_DISABLE_NEGOTIATE_NTLM option and detect gss_set_neg_mechs() availability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/curl_config-cmake.h.in Outdated
@mjcheetham

mjcheetham commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

I think we are ready to accept this PR without build option and without feature flag and just completely disable NTLM in SPNEGO. We just need to wait for the feature window to open again, which it should do on May 9 if nothing bad happens.

Then we can ship this in 8.21.0 in June and then drop the rest of the NTLM as planned in September for the release coming in October (probably 8.23.0).

@bagder, sounds good! Just pushed the next update with more squashed fixups:

  • drop commit 6; no build option anymore
  • address Copilot's comment about missing #cmakedefine
  • reword commit 5's message to drop references to now-droped build option
Range-diff vs last push
  • 1: 9ba4ddb = 1: 9ba4ddb auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX
  • 2: 25a742e = 2: 25a742e spnego/sspi: block NTLM via PackageList exclusion
  • 3: 8e5ce53 ! 3: bd18dba spnego/gss-api: block NTLM via gss_set_neg_mechs
        @@ docs/INSTALL-CMAKE.md: Available variables:
          - `HAVE_LDAP_URL_PARSE`:                    `ldap_url_parse` present in LDAP library.
          - `HAVE_MBEDTLS_DES_CRYPT_ECB`:             `mbedtls_des_crypt_ecb` present in mbedTLS <4.
    
        + ## lib/curl_config-cmake.h.in ##
        +@@
        + /* 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}
        +
        +
          ## lib/curl_gssapi.c ##
         @@ lib/curl_gssapi.c: OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data,
                                              gss_buffer_t input_token,
  • 4: e9bb2a6 = 4: cc3b7b7 gss-api: stub gss_inquire_context for debug builds
  • 5: 1a3f8b8 ! 5: a9f56a7 tests: add SPNEGO NTLM blocking tests
        @@ Metadata
          ## Commit message ##
             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.
        +    test2092 verifies that when only NTLM credentials are available,
        +    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.
        +    test2093 verifies that Kerberos credentials still succeed when NTLM
        +    is blocked within SPNEGO.
    
             Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
    
        @@ tests/data/test2092 (new)
         +http
         +</server>
         +<name>
        -+SPNEGO skips auth when NTLM blocked by CURL_DISABLE_NEGOTIATE_NTLM
        ++SPNEGO skips auth when only NTLM credentials are available
         +</name>
         +<features>
         +GSS-API
        @@ tests/data/test2093 (new)
         +http
         +</server>
         +<name>
        -+SPNEGO with Kerberos still works when built with CURL_DISABLE_NEGOTIATE_NTLM
        ++SPNEGO with Kerberos still works when NTLM is blocked
         +</name>
         +<features>
         +GSS-API
  • 6: a8c82c8 < -: ---------- spnego: add --disable-negotiate-ntlm compile-time option

@mjcheetham

Copy link
Copy Markdown
Contributor Author

Build failures because Curl_safefree was replaced with curlx_safefree in master (commit 0df6c01). Rebasing onto master...

@mjcheetham

Copy link
Copy Markdown
Contributor Author

Adding guards for stubs when the Kerberos impl does not have gss_set_neg_mechs (like in Heimdal iirc).

Range-diff
  • 1: 953df06 = 1: 953df06 auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX
  • 2: 5107a87 = 2: 5107a87 spnego/sspi: block NTLM via PackageList exclusion
  • 3: 008c4d9 = 3: 008c4d9 spnego/gss-api: block NTLM via gss_set_neg_mechs
  • 4: 57d8775 ! 4: 2729b1a gss-api: extend stubs for SPNEGO NTLM blocking
        @@ lib/curl_gssapi.c: OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min,
         +  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)
        @@ lib/curl_gssapi.c: OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min,
         +
         +  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)
        @@ lib/curl_gssapi.h: OM_uint32 Curl_gss_delete_sec_context(OM_uint32 *min,
         +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);
  • 5: 3f056a4 = 5: 7a28caf tests: add SPNEGO NTLM blocking test

@mjcheetham

Copy link
Copy Markdown
Contributor Author

I think the remaining CI failure (CM libressl krb5 valgrind 2) is a valgrind false positive rather than a logic error.

The test itself seems to pass with exit 0 + no Authorization header + correct protocol output. But valgrind flags a 32-byte leak inside Heimdal's gss_display_status, maybe because it allocates a status string that never gets freed:

==9260== 32 bytes in 1 blocks are definitely lost in loss record 28 of 67
==9260==    at 0x488C2D8: realloc (vgpreload_memcheck-arm64-linux.so)
==9260==    by 0x505A097: ??? (libgssapi_krb5.so.2.2)
==9260==    by 0x50337C7: gss_display_status (libgssapi_krb5.so.2.2)
==9260==    by 0x492C83F: display_gss_error (libcurl.so)
==9260==    by 0x492C9AB: Curl_gss_log_error (libcurl.so)
==9260==    by 0x49DD48F: Curl_auth_decode_spnego_message (libcurl.so)

Should I add a valgrind suppression for gss_display_status in tests/valgrind.supp?

The other two failing checks seem unrelated:

  • CM clang OpenSSL event-based — looks like a flake?
  • mingw AM ucrt-x86_64 c-ares — didn't start correctly, maybe a GitHub runner issue

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 <mjcheetham@outlook.com>
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 <mjcheetham@outlook.com>
Block NTLM within GSS-API SPNEGO negotiation. Acquire explicit
credentials, enumerate available mechanisms, filter out the NTLMSSP
OID, and restrict via gss_set_neg_mechs(). As a belt-and-braces
check, also verify the negotiated mechanism after context
establishment and reject NTLM if it was selected.

Add a cred_handle parameter to Curl_gss_init_sec_context so SPNEGO
can pass the restricted credentials.

Probe for gss_set_neg_mechs() availability (HAVE_GSS_SET_NEG_MECHS)
in configure and CMake. Not all GSS-API implementations provide
gss_set_neg_mechs(); when it is absent the proactive filtering is
skipped and the post-handshake mechanism check is the sole guard.

Update test2057: with NTLM now unconditionally blocked in SPNEGO,
the previous expectation of a successful multi-round NTLM handshake
no longer holds. The test now verifies that when only NTLM stub
credentials are available, negotiate auth is silently skipped and
the request is sent without an Authorization header.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Extend the GSS-API debug stub layer to support the NTLM blocking
logic without a real Kerberos environment.

Add stub_gss_acquire_cred (no-op success), stub_gss_indicate_mechs
(returns KRB5 and/or NTLMSSP OIDs based on CURL_STUB_GSS_CREDS),
and stub_gss_inquire_context (returns the negotiated mechanism OID).

Wrap these behind Curl_gss_acquire_cred, Curl_gss_indicate_mechs,
and Curl_gss_inquire_context so the stubs are transparently selected
when CURL_STUB_GSS_CREDS is set. Update spnego_gssapi.c to use the
wrappers.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
Add test2093 which verifies that Kerberos credentials still succeed
when NTLM is blocked within SPNEGO.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
@mjcheetham
mjcheetham force-pushed the spnego-no-ntlm-v3 branch from 4eaccd9 to 729b0c4 Compare May 11, 2026 14:13
@mjcheetham

Copy link
Copy Markdown
Contributor Author

Rebased on latest master..

Range-diff
  • 1: 953df06 = 1: 87758ef auth: upgrade SSPI identity to SEC_WINNT_AUTH_IDENTITY_EX
  • 2: 5107a87 = 2: 118c34f spnego/sspi: block NTLM via PackageList exclusion
  • 3: 008c4d9 ! 3: 75c7481 spnego/gss-api: block NTLM via gss_set_neg_mechs
        @@ lib/curl_gssapi.h: OM_uint32 Curl_gss_init_sec_context(struct Curl_easy *data,
                                                gss_ctx_id_t *context,
    
          ## lib/socks_gssapi.c ##
        -@@ lib/socks_gssapi.c: CURLcode Curl_SOCKS5_gssapi_negotiate(struct Curl_cfilter *cf,
        +@@ lib/socks_gssapi.c: static CURLcode socks5_gss_auth_loop(struct Curl_cfilter *cf,
                                                           gss_token,
                                                           &gss_send_token,
                                                           TRUE,
        --                                                 &gss_ret_flags);
        -+                                                 &gss_ret_flags,
        +-                                                 gss_ret_flags);
        ++                                                 gss_ret_flags,
         +                                                 GSS_C_NO_CREDENTIAL);
    
              if(gss_token != GSS_C_NO_BUFFER) {
  • 4: 1788718 = 4: 3d63443 gss-api: extend stubs for SPNEGO NTLM blocking
  • 5: 4eaccd9 ! 5: 729b0c4 tests: add SPNEGO NTLM blocking test
        @@ Metadata
          ## Commit message ##
             tests: add SPNEGO NTLM blocking test
    
        -    Add test2092 which verifies that Kerberos credentials still succeed
        +    Add test2093 which verifies that Kerberos credentials still succeed
             when NTLM is blocked within SPNEGO.
    
             Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
        @@ tests/data/Makefile.am: test2056 test2057 test2058 test2059 test2060 test2061 te
          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 \
        +-test2088 test2089 test2090 test2091 test2092 \
        ++test2088 test2089 test2090 test2091 test2092 test2093 \
          test2100 test2101 test2102 test2103 test2104 \
          \
        - test2200 test2201 test2202 test2203 test2204 test2205 \
        + test2200 test2201 test2202 test2203 test2204 test2205 test2206 test2207 \
    
        - ## tests/data/test2092 (new) ##
        + ## tests/data/test2093 (new) ##
         @@
         +<?xml version="1.0" encoding="US-ASCII"?>
         +<testcase>

But looks like GitHub is having issues with Git clones. Several CI checks are failing with failure to checkout actions; even 500 errors from git clones on the AppVeyor runs! Yikes GitHub!

@mjcheetham
mjcheetham requested a review from vszakats May 11, 2026 14:31
MIT Kerberos' libgssapi_krb5 has an internal allocation in
gss_display_status() that is never freed, even when callers correctly
release the returned buffer via gss_release_buffer(). Valgrind's
leak-check therefore flags a 'definitely lost' block whenever curl
formats a GSS-API error message via Curl_gss_log_error().

The leak is in the system library, not in curl.

Add a Memcheck:Leak suppression matching the stack:

  realloc -> ... -> gss_display_status -> display_gss_error
                                       -> Curl_gss_log_error

The wildcard '...' bridges over an anonymous frame inside
libgssapi_krb5.so that valgrind reports as '???'.

This unblocks the CM libressl krb5 valgrind 2 CI job, which uses MIT
Kerberos as the system GSS-API and previously failed test 2057 on the
valgrind leak rather than on any test logic mismatch.

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

Copy link
Copy Markdown
Contributor Author

Added a new commit to suppress the MIT krb5 gss_display_status leak under valgrind.

From the CM libressl krb5 valgrind 2 job:

test 2057...[HTTP Negotiate authentication blocked for stub NTLM credentials]
 valgrind ERROR ==9199== 32 bytes in 1 blocks are definitely lost in loss record 28 of 67
==9199==    at 0x488C2D8: realloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==9199==    by 0x505A097: ??? (in /usr/lib/aarch64-linux-gnu/libgssapi_krb5.so.2.2)
==9199==    by 0x50337C7: gss_display_status (in /usr/lib/aarch64-linux-gnu/libgssapi_krb5.so.2.2)
==9199==    by 0x492CE1F: display_gss_error (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x492CF8B: Curl_gss_log_error (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x49DE933: Curl_auth_decode_spnego_message (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x497251F: Curl_input_negotiate (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x497277F: Curl_output_negotiate (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x49574EB: output_auth_headers (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x4957B17: Curl_http_output_auth (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x495D033: Curl_http (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x49896E7: multi_do (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x498A643: multistate_do (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x498BD2B: multi_runsingle (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x498C0AF: multi_perform (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199==    by 0x498C36B: curl_multi_perform (in /home/runner/work/curl/curl/bld/lib/libcurl.so.4.8.0)
==9199== 

An internal allocation in libgssapi_krb5.so is never freed even when callers correctly release the returned buffer (which Curl_gss_log_error absolutely does). The suppression in tests/valgrind.supp matches the leak by stack signature.

This should unblock the CM libressl krb5 valgrind 2 CI job.

@mjcheetham

Copy link
Copy Markdown
Contributor Author

@bagder @vszakats I think we've finally got the PR is a stable place, with green runs. NTLM-over-SPNEGO is unconditionally blocked or prevented for both SSPI/Windows and GSS-API/Unix. Thanks for your patience!

@mjcheetham

Copy link
Copy Markdown
Contributor Author

@bagder @vszakats gentle ping? Thanks!

@jay

jay commented Jun 13, 2026

Copy link
Copy Markdown
Member

I think we are ready to accept this PR without build option and without feature flag and just completely disable NTLM in SPNEGO. We just need to wait for the feature window to open again, which it should do on May 9 if nothing bad happens.

Then we can ship this in 8.21.0 in June and then drop the rest of the NTLM as planned in September for the release coming in October (probably 8.23.0).

@bagder it looks like this was missed? the author gave two pings. is it now intended to go straight to drop in sep?

@bagder

bagder commented Jun 13, 2026

Copy link
Copy Markdown
Member

Yes, unfortunately this and a few more PRs didn't make it during this feature window primarily because I was too occupied handling vulnerability reports. I mentioned it in my May 28 email.

@jay

jay commented Jul 24, 2026

Copy link
Copy Markdown
Member

Ref: https://devblogs.microsoft.com/devops/upcoming-change-ntlm-removal-in-git-libcurl-impact-to-azure-devops-server-customers/

@bagder can you clarify, should this PR be closed and then drop NTLM in September?

@bagder

bagder commented Jul 24, 2026

Copy link
Copy Markdown
Member

@bagder can you clarify, should this PR be closed and then drop NTLM in September?

I think we can merge this already in this window, as a first small step. We just need to fix the merge conflicts...

@bagder

bagder commented Jul 27, 2026

Copy link
Copy Markdown
Member

@mjcheetham if you can fix the merge conflicts and force-push here, I think we might be ready to merge!

@jay

jay commented Jul 27, 2026

Copy link
Copy Markdown
Member

Thanks @mjcheetham. @dscho rebased this as #22410 so we're going with that and no more change is necessary on your part.

vszakats

This comment was marked as outdated.

jay pushed a commit to jay/curl that referenced this pull request Jul 27, 2026
- 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 curl#21315
Closes curl#22410
@jay jay closed this in a8881e5 Jul 27, 2026
@mjcheetham

Copy link
Copy Markdown
Contributor Author

Thank you all, and thanks @dscho for rebasing for me whilst I was out yesterday!

vszakats added a commit that referenced this pull request Jul 28, 2026
- update `GSS_C_DELEG_POLICY_FLAG` comment to include Apple GSS, add
  date, and amend MIT Kerberos version to 1.7+ (was: 1.8+)
  Ref: krb5/krb5@45875a4
  Ref: apple-oss-distributions/Heimdal@1635de3

- document `HAVE_GSS_SET_NEG_MECHS`/`gss_set_neg_mechs()`.
  Ref: krb5/krb5@079eed2
  It's also committed to Heimdal, but not present in a release
  as of 7.8.0 (current latest).
  Ref: heimdal/heimdal@735039d

Follow-up to a8881e5 #21315 #22410
Follow-up to d169ad6 #22052

Closes #22419
@mjcheetham
mjcheetham deleted the spnego-no-ntlm-v3 branch August 4, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-window A merge of this requires an open feature window libcurl API tests

Development

Successfully merging this pull request may close these issues.

8 participants