Skip to content

vauth: clear SSPI credentials on AcquireCredentialsHandle failure#21642

Closed
pen-pal wants to merge 4 commits into
curl:masterfrom
pen-pal:sspi-clear-credentials-on-acquire-failure
Closed

vauth: clear SSPI credentials on AcquireCredentialsHandle failure#21642
pen-pal wants to merge 4 commits into
curl:masterfrom
pen-pal:sspi-clear-credentials-on-acquire-failure

Conversation

@pen-pal
Copy link
Copy Markdown
Contributor

@pen-pal pen-pal commented May 16, 2026

Summary

Three small SSPI cleanup patches across lib/vauth/:

  • krb5_sspi.c — clear krb5->credentials after a failed AcquireCredentialsHandle()
  • spnego_sspi.c — clear nego->credentials after a failed AcquireCredentialsHandle()
  • ntlm_sspi.c — clear ntlm->credentials after a failed AcquireCredentialsHandle()

The problem

Per the Microsoft SSPI contract for AcquireCredentialsHandle, on failure the output handle is invalid and must not be passed to any other security function.
The current code in all three sites leaves the heap-allocated CredHandle buffer referenced from the auth struct, which produces two real-world consequences:

  1. API misuse on cleanup. Each protocol's cleanup function (Curl_auth_cleanup_gssapi, Curl_auth_cleanup_spnego, Curl_auth_cleanup_ntlm) later calls FreeCredentialsHandle() on the calloc'd-but-invalid handle. In practice all Windows SSPI
    providers I'm aware of return SEC_E_INVALID_HANDLE and do not crash, but this is API misuse and not contract-safe across SSPI provider implementations.

  2. State-machine wart (krb5, spnego only). The outer if(!field->credentials) guard at the top of each function skips re-initialization on subsequent calls. After a transient AcquireCredentialsHandle failure, the field stays non-NULL but invalid, so the next attempt at authentication on the same connection re-enters with a dead handle and InitializeSecurityContext fails — the connection's auth is wedged until the connection itself is torn down. NTLM is not affected by this aspect since it calls Curl_auth_cleanup_ntlm at function entry.

The fix

On the AcquireCredentialsHandle failure path, curlx_free the buffer and set the field to NULL. After the change:

  • The cleanup function's if(field->credentials) guard correctly skips the invalid handle.
  • For krb5 and spnego, the outer guard allows the next call to re-attempt initialization.
  • No behavior change in the success path.

Testing

  • Build: cross-compiled clean for x86_64-w64-mingw32 (gcc 15.2.0) with --enable-sspi. Zero warnings or errors across the full libcurl build. The three patched objects emit as PE/COFF.
  • Style: make checksrc passes.
  • Runtime: I do not have a Windows development environment, so I have not exercised the patched paths against live Kerberos/Negotiate/NTLM providers. Reviewers with Windows CI access would help confirm by hitting a deliberately broken auth scenario (bad principal, unreachable KDC, etc.) and verifying retry behavior.

pen-pal added 3 commits May 15, 2026 23:38
Per the Microsoft SSPI contract, when AcquireCredentialsHandle() fails
the output handle contents are undefined and must not be passed to any
other security function. The current code leaves krb5->credentials as
a non-NULL calloc'd buffer, which means:

  - Curl_auth_cleanup_gssapi() later calls FreeCredentialsHandle() on
    an invalid handle. SSPI providers tend to handle this gracefully
    (returning SEC_E_INVALID_HANDLE) but it is technically API misuse.
  - The outer "if(!krb5->credentials)" guard in this function skips
    re-initialization on retry, so a transient acquire failure leaves
    the connection's auth permanently broken until teardown.

Free the buffer and NULL the pointer on failure so retries can
re-initialize and the cleanup path skips the invalid handle.

Build-tested on Windows target via mingw-w64 (x86_64-w64-mingw32-gcc
15.2.0) cross-compile with --enable-sspi; runtime behavior against
live Kerberos KDCs not exercised.
…lure

Per the Microsoft SSPI contract, when AcquireCredentialsHandle() fails
the output handle contents are undefined and must not be passed to any
other security function. The current code leaves nego->credentials as
a non-NULL calloc'd buffer, which means:

  - Curl_auth_cleanup_spnego() later calls FreeCredentialsHandle() on
    an invalid handle. SSPI providers tend to handle this gracefully
    (returning SEC_E_INVALID_HANDLE) but it is technically API misuse.
  - The outer "if(!nego->credentials)" guard in this function skips
    re-initialization on retry, so a transient acquire failure leaves
    the connection's auth permanently broken until teardown.

Free the buffer and NULL the pointer on failure so retries can
re-initialize and the cleanup path skips the invalid handle.

Build-tested on Windows target via mingw-w64 (x86_64-w64-mingw32-gcc
15.2.0) cross-compile with --enable-sspi; runtime behavior against
live Negotiate providers not exercised.
Per the Microsoft SSPI contract, when AcquireCredentialsHandle() fails
the output handle contents are undefined and must not be passed to any
other security function. Currently, on failure ntlm->credentials stays
set to the calloc'd buffer, and the next call's Curl_auth_cleanup_ntlm
pass (run at the top of Curl_auth_create_ntlm_type1_message) calls
FreeCredentialsHandle() on the invalid handle.

In practice Windows SSPI providers return SEC_E_INVALID_HANDLE without
crashing, but this is API misuse. Free the buffer and NULL the pointer
on failure so cleanup skips the invalid handle.

Build-tested on Windows target via mingw-w64 (x86_64-w64-mingw32-gcc
15.2.0) cross-compile with --enable-sspi; runtime behavior against
live NTLM providers not exercised.
@jay
Copy link
Copy Markdown
Member

jay commented May 16, 2026

the comments aren't needed and you can use curlx_safefree to null it for example

    if(status != SEC_E_OK) {
      curlx_safefree(krb5->credentials);
      return CURLE_LOGIN_DENIED;
    }

That said I'm not sure this is needed. FreeCredentialsHandle does not crash on invalid handle and for the 2nd problem I don't think we can end up after a login denied where the credentials is used but better safe than sorry I guess so I'm not opposed.

@bagder
Copy link
Copy Markdown
Member

bagder commented May 16, 2026

I'm just a bit concerned that this is done blindly (with an AI) without even testing it manually once.

@jay
Copy link
Copy Markdown
Member

jay commented May 20, 2026

I'm just a bit concerned that this is done blindly (with an AI) without even testing it manually once.

Considering what it does I think it's fine. He's clearing the credentials for something that may happen and I don't see a need to find a code path where it does happen because it's good practice to do that. Generally, FreeCredentialsHandle on zeroed credentials or credentials from a failed AcquireCredentialsHandle is ok, we do it elsewhere, however in the case of SSPI he's right that we're evaluating credentials as a condition of initialization. So, hallucination or not it seems like a good idea for SSPI.

@jay jay closed this in ba7b65f May 20, 2026
@jay
Copy link
Copy Markdown
Member

jay commented May 20, 2026

Thanks

outcast36 pushed a commit to greearb/curl that referenced this pull request Jun 3, 2026
- Clear credentials on AcquireCredentialsHandle failure so it is not
  used on a subsequent call.

SSPI initialization may evaluate the credentials pointer to determine
whether or not a prior call to AcquireCredentialsHandle was successful,
therefore we must clear it on a failed call.

Closes curl#21642
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.

3 participants