Skip to content

spnego: add options to block NTLM fallback and fail on error#21076

Open
mjcheetham wants to merge 8 commits intocurl:masterfrom
mjcheetham:spnego-no-ntlm
Open

spnego: add options to block NTLM fallback and fail on error#21076
mjcheetham wants to merge 8 commits intocurl:masterfrom
mjcheetham:spnego-no-ntlm

Conversation

@mjcheetham
Copy link
Copy Markdown

It may be best to review this PR commit-by-commit, as the two options are related but logically separate. Commits in this PR are based on curl-8_18_0.

Note: 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. These changes address that gap.

Motivation

Imagine you carefully configure your application to use Negotiate authentication, expecting the strong mutual authentication guarantees of Kerberos. Behind the scenes, SPNEGO silently decides that Kerberos isn't available — maybe a misconfigured SPN, maybe a clock-skew issue, maybe a missing ticket — and falls back to NTLM without telling anyone. Your application happily continues, now authenticated with a protocol that doesn't verify the server's identity, is vulnerable to relay attacks, and sends credentials derived from the user's password. Worse still, if the NTLM exchange also fails, libcurl shrugs and sends the request anyway — completely unauthenticated — and returns success. The calling application has no idea any of this happened. This is the default behaviour today.

NTLM-over-SPNEGO blocking (CURLOPT_SPNEGO_NTLM_ALLOWED)

Commits 1-4, 8

SPNEGO (Negotiate) authentication can silently fall back to NTLM when Kerberos is unavailable or misconfigured. This is a problem in environments that require Kerberos — rather than failing loudly so the misconfiguration can be detected, the client quietly downgrades to the weaker NTLM protocol.

This fallback behaviour could be construed as a security issue, but is also well-defined behaviour of SPNEGO (RFC 4178 section 3). Since SPNEGO defines that GSS-API implementations can select from different security mechanism packages, applications with a stronger security stance have no way to restrict the use of NTLM at the application level.

On Windows, SSPI's Negotiate SSP includes NTLM with no easy way to prevent it — "the Negotiate security package selects between Kerberos and NTLM. Negotiate selects Kerberos unless it can't be used by one of the systems involved in the authentication [or] the calling app didn't provide sufficient information to use Kerberos." (See also [MS-SPNG].) The only option is to disable NTLM system-wide via the RestrictSendingNTLMTraffic registry key (set to 0x2), which is a big hammer that affects every application on the machine, not just the one that wants Kerberos-only SPNEGO.

On other platforms, plugins such as gss-ntlmssp can be pulled in via dependencies, adding NTLM support to MIT Kerberos's GSS-API and enabling the same NTLM-over-SPNEGO fallback there too.

This PR adds CURLOPT_SPNEGO_NTLM_ALLOWED (default 1L / true). When set to 0L, libcurl checks the sub-mechanism negotiated by SPNEGO after the security context is initialized but before any tokens are sent on the wire. If NTLM was selected, authentication is aborted. Bare NTLM auth (CURLAUTH_NTLM) is unaffected.

Detection uses platform-native APIs:

  • Windows (SSPI): QueryContextAttributes with SECPKG_ATTR_NEGOTIATION_INFO
  • Unix (GSS-API): gss_inquire_sec_context comparing the actual mechanism OID against the NTLMSSP OID

The CLI equivalent is --spnego-ntlm-allowed / --no-spnego-ntlm-allowed.

Fail on SPNEGO error (CURLOPT_SPNEGO_FAIL_ON_ERROR)

Commits 5-7

Currently, when SPNEGO authentication fails for any reason (no Kerberos ticket, disallowed mechanism, etc.), libcurl silently continues and sends the request unauthenticated. While blocking NTLM-over-SPNEGO (above) is not itself a security issue — NTLM credentials are never sent on the wire — the calling application has no way to know that SPNEGO failed unless the transfer itself is inspected.

Is this option strictly necessary? Arguably not — CURLOPT_SPNEGO_NTLM_ALLOWED alone prevents NTLM credentials from being sent, and the server will typically reject the unauthenticated request anyway. However, this is a belts-and-braces approach: rather than relying on the server to reject the request, the client can detect the failure early and surface it explicitly. Without this option, a caller that sets CURLOPT_SPNEGO_NTLM_ALLOWED to false would see what looks like a successful transfer (HTTP 200 from a public endpoint, or a 401/403 with no indication why), with no signal from libcurl that SPNEGO was even attempted and failed.

This is also inconsistent with how curl treats other authentication methods. As @bagder noted in #3726 (the outcome of which was a PR that restored the 'continue-on-spnego-fail' behaviour):

curl never "falls back" to another authentication method. It picks one and goes with that. If that fails to work/authenticate, it returns error. (I would suggest anything else is a bug.)

This PR adds CURLOPT_SPNEGO_FAIL_ON_ERROR (default 0L / false for backward compatibility). When set to 1L, libcurl returns CURLE_AUTH_ERROR instead of continuing unauthenticated when SPNEGO authentication fails.

The CLI equivalent is --spnego-fail-on-error.

Commits

  1. spnego: add CURLOPT_SPNEGO_NTLM_ALLOWED — core libcurl option and SSPI/GSS-API detection logic
  2. tool: add --spnego-ntlm-allowed option — CLI flag
  3. tests/unit3217 — setopt plumbing tests
  4. gss-api: stub gss_inquire_context for debug builds — enables testing without a real Kerberos environment
  5. spnego: add CURLOPT_SPNEGO_FAIL_ON_ERROR — core libcurl option
  6. tool: add --spnego-fail-on-error option — CLI flag
  7. tests/unit3218 — setopt plumbing tests
  8. tests: add SPNEGO NTLM blocking tests — integration tests (test2092, test2093)

Open questions

  • Should CURLOPT_SPNEGO_NTLM_ALLOWED default to FALSE?

    Keeping it TRUE by default maintains backward compatibility, but is this the best security stance? Even Microsoft now recommends moving away from NTLM — in Windows Server 2025, NTLMv1 has been removed and NTLMv2 is deprecated, with the guidance: "Replace calls to NTLM with calls to Negotiate, which tries to authenticate with Kerberos and only falls back to NTLM when necessary." If the industry is moving to deprecate NTLM entirely, should curl be secure-by-default here rather than compatible-by-default?

  • Should CURLOPT_SPNEGO_FAIL_ON_ERROR default to TRUE?

    Similar question around backward compatibility. The current silent failure behaviour may be argued to be a bug and inconsistent with how other auth methods are treated, but changing the default would be a breaking change (see the earlier issue that was raised). Where does curl stand on breaking changes (with a 'quirks' opt-in for legacy behaviour) vs maintaining backward compatibility by default?

  • Should SSPI be stubbed/wrapped for testing, like GSS-API is?

    This PR adds a GSS-API stub (gss_inquire_context etc.) so the NTLM-detection logic can be exercised in debug builds without a real Kerberos environment. The SSPI code path has no equivalent — it calls the real Windows APIs directly and can only be tested on Windows with actual credentials. Would this be a good time to introduce an SSPI stub/wrapper layer (similar to the existing Curl_gss_* wrappers) so that the SSPI code paths can also be tested in CI without a domain-joined environment?

SPNEGO (Negotiate) authentication can silently fall back to NTLM when
Kerberos is unavailable. This is undesirable in environments that
require Kerberos and want to detect misconfiguration rather than
quietly downgrade to a weaker protocol.

Add CURLOPT_SPNEGO_NTLM_ALLOWED (default 1L). When set to 0L, libcurl
checks the sub-mechanism selected by SPNEGO after the security context
is initialized but before any tokens are sent on the wire. If NTLM was
chosen, the authentication fails with CURLE_AUTH_ERROR.

On Windows (SSPI), detection uses QueryContextAttributes with
SECPKG_ATTR_NEGOTIATION_INFO. On Unix (GSS-API), it uses
gss_inquire_sec_context to compare the actual mechanism OID against the
NTLMSSP OID.

Bare NTLM auth (CURLAUTH_NTLM) is not affected.

Signed-off-by: Matthew John Cheetham <[email protected]>
Add the --spnego-ntlm-allowed command-line flag (negated with
--no-spnego-ntlm-allowed) that maps to CURLOPT_SPNEGO_NTLM_ALLOWED,
allowing users to block NTLM fallback during SPNEGO authentication
from the curl CLI.

Signed-off-by: Matthew John Cheetham <[email protected]>
Test that the option is accepted by curl_easy_setopt, that its default
is TRUE (NTLM allowed), and that setting it to 0L and 1L correctly
updates the internal state. Guarded by USE_SPNEGO so the test body is
skipped on builds without Negotiate support.

Signed-off-by: Matthew John Cheetham <[email protected]>
The GSS-API debug stub did not implement gss_inquire_context, so
the NTLM-detection logic in spnego_gssapi.c could not be exercised
without a real Kerberos environment.

Add stub_gss_inquire_context that returns the NTLMSSP OID when the
stub context is in NTLM mode and the Kerberos OID otherwise. Wrap it
behind Curl_gss_inquire_context so the stub is transparently
selected when CURL_STUB_GSS_CREDS is set.

Signed-off-by: Matthew John Cheetham <[email protected]>
By default, when SPNEGO (Negotiate) authentication fails (for example,
no Kerberos ticket available, or the negotiated mechanism is disallowed
via CURLOPT_SPNEGO_NTLM_ALLOWED), libcurl silently continues and sends
the request unauthenticated for backward compatibility.

Add CURLOPT_SPNEGO_FAIL_ON_ERROR (default 0L). When set to 1L, libcurl
returns CURLE_AUTH_ERROR instead of continuing unauthenticated when
SPNEGO authentication fails.

Signed-off-by: Matthew John Cheetham <[email protected]>
Add the --spnego-fail-on-error command-line flag that maps to
CURLOPT_SPNEGO_FAIL_ON_ERROR, allowing users to make curl return an
error when SPNEGO authentication fails instead of silently continuing
unauthenticated.

Signed-off-by: Matthew John Cheetham <[email protected]>
Test that the option is accepted by curl_easy_setopt, that its default
is FALSE, and that setting it to 0L and 1L correctly updates the
internal state. Guarded by USE_SPNEGO so the test body is skipped on
builds without Negotiate support.

Signed-off-by: Matthew John Cheetham <[email protected]>
test2092 verifies that --no-spnego-ntlm-allowed rejects NTLM-only
SPNEGO credentials with CURLE_AUTH_ERROR (94) (when
--spnego-fail-on-error is also set).

test2093 verifies that Kerberos credentials still succeed when
--no-spnego-ntlm-allowed is set.

Signed-off-by: Matthew John Cheetham <[email protected]>
@dfandrich
Copy link
Copy Markdown
Contributor

dfandrich commented Mar 23, 2026 via email

Curl_auth_cleanup_spnego(nego);
return CURLE_AUTH_ERROR;
}
}
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 do remember that there is a way to disable NTLM as a package upfront before you start the security context. I can try to look the code up if you like. It was done by @wangweij for the SSPI Java bridge several years ago.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That would be great if possible!

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll first need to 'upgrade' the use of the SEC_WINNT_AUTH_IDENTITY structure to the _EX variant since the PackageList field only exists from that point. The _EX2 variant was only introduced in Windows 7/2008 R2 so that one is still too old to use in curl.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hmm.. in implementing this idea I realised something. In Git for Windows we'd like to be able to detect the situation that NTLM would have been used and not just prevent it with a generic error (CURLE_AUTH_ERROR) - we'd like to present this as a warning to the user in our scenario. We could be erroring because NTLM was blocked or because Kerberos failed.

If we switch to an up-front disablement of NTLM-over-SPNEGO then we cannot know if the server would have tried NTLM or not (i.e, was Kerberos working on not).

@dscho
Copy link
Copy Markdown
Contributor

dscho commented Mar 24, 2026

I don't think it's worth adding a new option for a feature (NTLM) that's going to be removed from curl entirely soon.

@dfandrich could you please define "soon"? If it's imminent, I agree that we don't need this. But if it's even just a couple of versions out, we need a way at least in Git for Windows to have more fine-grained control to disable NTLM than is available at present.

@mjcheetham
Copy link
Copy Markdown
Author

Rather than a run-time check, how about gating NTLM in SPNEGO use on whether NTLM has been enabled at configure time?

In an ideal world we'd just rip out (or guard against) these old protocols and auth mechanisms, however there may be reasons why an application using libcurl may wish to support things like NTLM in very specific scenarios where risks can be mitigated.

Perhaps a piece of missing context here is that Git for Windows would like to selectively disable NTLM outside of 'trusted' servers and environments.

@dfandrich
Copy link
Copy Markdown
Contributor

@dfandrich could you please define "soon"?

The plan is to remove it entirely in September, which means two more releases where someone could still enable it.

@mjcheetham
Copy link
Copy Markdown
Author

mjcheetham commented Mar 24, 2026

The plan is to remove it entirely in September, which means two more releases where someone could still enable it.

So this means the (bare) NTLM mechanism (CURLAUTH_NTLM) will be removed from curl in Sept this year, but (at least without this change) NTLM-over-SPNEGO would still be possible (with a curl compiled with SPNEGO support)?

@dfandrich
Copy link
Copy Markdown
Contributor

dfandrich commented Mar 24, 2026 via email

@mjcheetham
Copy link
Copy Markdown
Author

That's right. Which isn't to say that it should be that way.

@dfandrich, indeed. NTLM-over-SPNEGO is a gap that the NTLM removal in September doesn't close.

Sadly compile-time gating doesn't work for Git for Windows - we ship (and are shipped by others) one package to millions of users across different environments where some corporate networks still rely on NTLM during their transition off of it. We need per-transfer control rather than an all-or-nothing build flag.

I'm happy to mark this option as 'transitional' if that helps? The intent is to close the NTLM-over-SPNEGO control gap that exists.

@dfandrich
Copy link
Copy Markdown
Contributor

Basically what you want is a way to influence the negotiation that the CURLAUTH_NEGOTIATE authentication method itself does, which might have some merit except that it negotiates between only two methods, one of which will (potentially) be gone in a few months. It's also worked this way for just about 23 years without issue. I'm not the one to have the final say, but you haven't yet convinced me of the need for an option that would live in at most two, and probably only a single curl release (the feature freeze begins days from now). You can always carry a patch locally for your own needs as long as you'd like.

@mjcheetham
Copy link
Copy Markdown
Author

I'm not the one to have the final say, but you haven't yet convinced me of the need for an option that would live in at most two, and probably only a single curl release (the feature freeze begins days from now).

To clarify — this option isn't about bare CURLAUTH_NTLM, which yes, is going away in September (and arguably a good thing). It's about the NTLM fallback within CURLAUTH_NEGOTIATE/SPNEGO, which as you confirmed earlier, survives the September removal and has no planned removal date.

So this isn't a two-release option. It's relevant as long as SPNEGO support exists in curl, since SPNEGO can, by design, negotiate NTLM (or any SSP that is available to the OS).

Would it be more palatable if this were framed as a general-purpose mechanism filter for SPNEGO — an allowlist/denylist of sub-mechanisms (SSP package names on Windows, mechanism OIDs on GSS-API)?
That would make it a broader capability not tied to NTLM's lifecycle. Happy to explore that direction if there's interest.

You can always carry a patch locally for your own needs as long as you'd like.

This gap affects any libcurl consumer using SPNEGO on Windows, not just Git, so having it upstream benefits the broader ecosystem. That said we'd also be open to a middle ground: if curl accepted a compile-time flag to disable NTLM-over-SPNEGO, we could carry a much smaller local patch to make that flag runtime-configurable for our needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants