spnego: add options to block NTLM fallback and fail on error#21076
spnego: add options to block NTLM fallback and fail on error#21076mjcheetham wants to merge 8 commits intocurl:masterfrom
Conversation
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]>
|
The CLI equivalent is --spnego-ntlm-allowed / --no-spnego-ntlm-allowed.
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. Rather than a run-time check, how about
gating NTLM in SPNEGO use on whether NTLM has been enabled at configure time?
It is now off by default, so doing this will prevent NTLM downgrades by default
now.
|
| Curl_auth_cleanup_spnego(nego); | ||
| return CURLE_AUTH_ERROR; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
That would be great if possible!
There was a problem hiding this comment.
Here it is: https://github.com/openjdk/jdk17u/blob/e33f6f64d3f9fb6e0075ab8077462c3097282c87/src/java.security.jgss/windows/native/libsspi_bridge/sspi.cpp#L672-L700
along with the struct limiting the security packages for SPNEGO: https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_winnt_auth_identity_exa
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
@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. |
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. |
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 ( |
|
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. |
|
Basically what you want is a way to influence the negotiation that the |
To clarify — this option isn't about bare 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)?
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. |
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_NTLMfromCURLOPT_HTTPAUTHto prevent bare NTLM authentication, NTLM can still be used under the hood whenCURLAUTH_NEGOTIATEis 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
RestrictSendingNTLMTrafficregistry key (set to0x2), 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(default1L/true). When set to0L, 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:
QueryContextAttributeswithSECPKG_ATTR_NEGOTIATION_INFOgss_inquire_sec_contextcomparing the actual mechanism OID against the NTLMSSP OIDThe 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_ALLOWEDalone 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 setsCURLOPT_SPNEGO_NTLM_ALLOWEDto 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
@bagdernoted in #3726 (the outcome of which was a PR that restored the 'continue-on-spnego-fail' behaviour):This PR adds
CURLOPT_SPNEGO_FAIL_ON_ERROR(default0L/falsefor backward compatibility). When set to1L, libcurl returnsCURLE_AUTH_ERRORinstead of continuing unauthenticated when SPNEGO authentication fails.The CLI equivalent is
--spnego-fail-on-error.Commits
spnego: add CURLOPT_SPNEGO_NTLM_ALLOWED— core libcurl option and SSPI/GSS-API detection logictool: add --spnego-ntlm-allowed option— CLI flagtests/unit3217— setopt plumbing testsgss-api: stub gss_inquire_context for debug builds— enables testing without a real Kerberos environmentspnego: add CURLOPT_SPNEGO_FAIL_ON_ERROR— core libcurl optiontool: add --spnego-fail-on-error option— CLI flagtests/unit3218— setopt plumbing teststests: add SPNEGO NTLM blocking tests— integration tests (test2092, test2093)Open questions
Should
CURLOPT_SPNEGO_NTLM_ALLOWEDdefault toFALSE?Keeping it
TRUEby 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_ERRORdefault toTRUE?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_contextetc.) 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 existingCurl_gss_*wrappers) so that the SSPI code paths can also be tested in CI without a domain-joined environment?