CVE-2026-118002
Overview
Background
- `nsHttpNTLMAuth`
- Firefox’s NTLM HTTP authenticator. It is a process-wide singleton: the class holds
static StaticRefPtr<nsHttpNTLMAuth> gSingletonandGetOrCreate()hands the same instance to every channel that negotiates NTLM. - `ChallengeReceived` / `GenerateCredentials`
- The two authenticator entry points.
ChallengeReceivedis called when a server challenges with NTLM and decides policy (native vs. internal module, whether default credentials are allowed);GenerateCredentialslater builds the actual credential blob and is where credentials are emitted. - `CanUseDefaultCredentials(channel, isProxyAuth)`
- Policy check that returns whether the prefs permit sending the logged-in OS user’s identity (NTLM single-sign-on) to a given host/proxy. Only “approved” hosts may authenticate silently as the current user.
- `already_AddRefed` / `RefPtr` / `nsCOMPtr` / `do_QueryObject`
- Gecko refcounting idioms; the fix uses them to build and own a new per-channel state object.
Root Cause Analysis
The authenticator stored per-channel decisions on the shared singleton. nsHttpNTLMAuth::ChallengeReceived wrote two member fields — mUseNative (= true, later possibly set false when falling back to the internal NTLM module) and mAllowDefaultCredentials (= CanUseDefaultCredentials(channel, isProxyAuth)) — and nsHttpNTLMAuth::GenerateCredentials read them back later when producing credentials, gated by if (mUseNative && !mAllowDefaultCredentials && (user.IsEmpty() || pass.IsEmpty())) return NS_ERROR_ABORT;. Because gSingleton is one object serving all channels, these fields are shared mutable state across concurrent authentication exchanges. Two channels authenticating to different hosts interleave: channel A (an untrusted host, for which CanUseDefaultCredentials is false) runs ChallengeReceived and sets mAllowDefaultCredentials = false; before A reaches GenerateCredentials, channel B (a trusted/approved host) runs ChallengeReceived and overwrites mAllowDefaultCredentials = true. When A then runs GenerateCredentials, it reads B’s stale true, the guard no longer aborts, and A proceeds to authenticate to the untrusted host as the logged-in user — leaking the OS user’s default NTLM credentials to a host that policy forbids. mUseNative is subject to the same clobbering, affecting the native-vs-internal and client-cert code paths.
The fix removes the shared per-channel state from the singleton. A new refcounted nsNTLMContinuationState object carries mModule and mUseNative and is stored in the per-channel *continuationState (state.forget(continuationState)); GenerateCredentials recovers it via do_QueryObject(*continuationState) and reads useNative from it, so the native flag is now per-exchange. Critically, mAllowDefaultCredentials is deleted outright: instead of caching the policy result in ChallengeReceived and trusting it later, both sites now call CanUseDefaultCredentials(channel/authChannel, isProxyAuth) directly at the moment of decision (if (!*continuationState && CanUseDefaultCredentials(channel, isProxyAuth)) in ChallengeReceived, and if (useNative && (user.IsEmpty() || pass.IsEmpty()) && !CanUseDefaultCredentials(authChannel, isProxyAuth)) in GenerateCredentials). Both mUseNative and mAllowDefaultCredentials members are removed from nsHttpNTLMAuth.h. This matches the advisory’s “information disclosure due to race condition”: the raced field is a credential-exposure gate.
mAllowDefaultCredentials, mUseNative) was cached on a process-wide singleton, so a concurrent authentication to a trusted host could overwrite the gate and cause the logged-in user’s default credentials to be sent to an untrusted host.Attack Path
- Trigger concurrent NTLM An attacker-controlled site (or a resource it causes the browser to fetch) returns an NTLM challenge at the same time the victim’s browser is also authenticating NTLM to a legitimately approved host/proxy (inferred: concurrency is required and is the race’s precondition).
- Race `ChallengeReceived`
The approved host’s
ChallengeReceivedruns and sets the singleton’smAllowDefaultCredentials = true, overwriting thefalsethe untrusted host’s exchange had set. - Untrusted exchange reaches `GenerateCredentials`
For the attacker’s host,
GenerateCredentialsreads the stalemAllowDefaultCredentials == true, so theNS_ERROR_ABORTguard that normally blocks silent default-credential use is skipped. - Silent SSO to attacker With an empty user/password, the native module authenticates as the logged-in OS user to the attacker’s host.
- Primitive The attacker’s server receives an NTLM type-3 message computed from the victim’s domain account (NTLM hash/response), i.e. disclosure of the user’s credentials / an offline-crackable NTLM response to a host that policy forbids.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
nsNTLMContinuationStatenetwerk/protocol/http/nsHttpNTLMAuth.cpp |
modified | |
ifnetwerk/protocol/http/nsHttpNTLMAuth.cpp |
modified |
Files Changed
netwerk/protocol/http/nsHttpNTLMAuth.cppnetwerk/protocol/http/nsHttpNTLMAuth.h
Audit Directions
- State cached on singletons/staticsAudit other
nsIHttpAuthenticatorimplementations (e.g. Negotiate/Kerberos, SSPI) and any singleton that stores per-request decisions in member fields acrossChallengeReceived→GenerateCredentials. - Cached policy vs. re-checked policyLook for other places that cache the result of a security check (like
CanUseDefaultCredentials) and consume it later, where an intervening operation could invalidate the assumption. - Continuation/session-state misuseReview code that stores an
nsIAuthModuleor similar directly in*continuationStateand laterdo_QueryInterfaces it, to ensure per-channel isolation and correct typing (the fix switched to a typeddo_QueryObject). - Default-credential gatesTrace every path that can send OS/default credentials and confirm the host-approval check is evaluated against the correct, current channel at the point of emission.
Patch
diff --git a/netwerk/protocol/http/nsHttpNTLMAuth.cpp b/netwerk/protocol/http/nsHttpNTLMAuth.cpp
index 248dcca44f1..5a086515c69 100644
--- a/netwerk/protocol/http/nsHttpNTLMAuth.cpp
+++ b/netwerk/protocol/http/nsHttpNTLMAuth.cpp
@@ -33,6 +33,7 @@
#include "nsCRT.h"
#include "nsIChannel.h"
#include "nsNetUtil.h"
+#include "nsQueryObject.h"
#include "nsUnicharUtils.h"
namespace mozilla {
@@ -130,6 +131,29 @@ class nsNTLMSessionState final : public nsISupports {
};
NS_IMPL_ISUPPORTS0(nsNTLMSessionState)
+#define NS_NTLMCONTINUATIONSTATE_IID \
+ {0xb2db7a3e, 0x7608, 0x4e9b, {0xba, 0x0b, 0xd0, 0x1a, 0x2f, 0x4f, 0x97, 0x22}}
+
+// Per-channel continuation state. The authenticator is a process-wide
+// singleton, so anything decided in ChallengeReceived for one channel must be
+// kept here rather than on nsHttpNTLMAuth.
+class nsNTLMContinuationState final : public nsISupports {
+ ~nsNTLMContinuationState() = default;
+
+ public:
+ NS_INLINE_DECL_STATIC_IID(NS_NTLMCONTINUATIONSTATE_IID)
+ NS_DECL_ISUPPORTS
+
+ nsNTLMContinuationState(already_AddRefed<nsIAuthModule> aModule,
+ bool aUseNative)
+ : mModule(aModule), mUseNative(aUseNative) {}
+
+ const nsCOMPtr<nsIAuthModule> mModule;
+ // Whether mModule is the native NTLM implementation or the internal one.
+ const bool mUseNative;
+};
+NS_IMPL_ISUPPORTS(nsNTLMContinuationState, nsNTLMContinuationState)
+
//-----------------------------------------------------------------------------
already_AddRefed<nsIHttpAuthenticator> nsHttpNTLMAuth::GetOrCreate() {
@@ -156,9 +180,6 @@ nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel,
LOG(("nsHttpNTLMAuth::ChallengeReceived [ss=%p cs=%p]\n", *sessionState,
*continuationState));
- // Use the native NTLM if available
- mUseNative = true;
-
// NOTE: we don't define any session state, but we do use the pointer.
*identityInvalid = false;
@@ -169,11 +190,10 @@ nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel,
if (challenge.Equals("NTLM"_ns, nsCaseInsensitiveCStringComparator)) {
nsCOMPtr<nsIAuthModule> module;
-#ifdef MOZ_AUTH_EXTENSION
- // Remembered for GenerateCredentials, which has to tell an identity the
- // user left empty apart from one it may fill in from the OS.
- mAllowDefaultCredentials = CanUseDefaultCredentials(channel, isProxyAuth);
+ // Use the native NTLM if available
+ bool useNative = true;
+#ifdef MOZ_AUTH_EXTENSION
// Check to see if we should default to our generic NTLM auth module
// through UseGenericNTLM. (We use native auth by default if the
// system provides it.) If *sessionState is non-null, we failed to
@@ -183,7 +203,8 @@ nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel,
// Check for approved default credentials hosts and proxies. If
// *continuationState is non-null, the last authentication attempt
// failed so skip default credential use.
- if (!*continuationState && mAllowDefaultCredentials) {
+ if (!*continuationState &&
+ CanUseDefaultCredentials(channel, isProxyAuth)) {
// Try logging in with the user's default credentials. If
// successful, |identityInvalid| is false, which will trigger
// a default credentials attempt once we return.
@@ -225,7 +246,7 @@ nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel,
LOG(("Trying to fall back on internal ntlm auth.\n"));
module = nsIAuthModule::CreateInstance("ntlm");
- mUseNative = false;
+ useNative = false;
// Prompt user for domain, username, and password.
*identityInvalid = true;
@@ -240,7 +261,9 @@ nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel,
// A non-null continuation state implies that we failed to authenticate.
// Blow away the old authentication state, and use the new one.
- module.forget(continuationState);
+ RefPtr<nsNTLMContinuationState> state =
+ new nsNTLMContinuationState(module.forget(), useNative);
+ state.forget(continuationState);
}
return NS_OK;
}
@@ -275,8 +298,12 @@ nsHttpNTLMAuth::GenerateCredentials(
if (user.IsEmpty() || pass.IsEmpty()) *aFlags = USING_INTERNAL_IDENTITY;
nsresult rv;
- nsCOMPtr<nsIAuthModule> module = do_QueryInterface(*continuationState, &rv);
- NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<nsNTLMContinuationState> state = do_QueryObject(*continuationState);
+ if (!state || !state->mModule) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsCOMPtr<nsIAuthModule> module = state->mModule;
+ const bool useNative = state->mUseNative;
void *inBuf, *outBuf;
uint32_t inBufLen, outBufLen;
@@ -286,8 +313,8 @@ nsHttpNTLMAuth::GenerateCredentials(
if (aChallenge.Equals("NTLM"_ns, nsCaseInsensitiveCStringComparator)) {
// An empty user or password makes nsAuthSSPI::Init authenticate as the
// logged-in user, which only CanUseDefaultCredentials() hosts may do.
- if (mUseNative && !mAllowDefaultCredentials &&
- (user.IsEmpty() || pass.IsEmpty())) {
+ if (useNative && (user.IsEmpty() || pass.IsEmpty()) &&
+ !CanUseDefaultCredentials(authChannel, isProxyAuth)) {
LOG(("Not using default credentials for an untrusted host\n"));
return NS_ERROR_ABORT;
}
@@ -333,7 +360,7 @@ nsHttpNTLMAuth::GenerateCredentials(
rv = channel->GetSecurityInfo(getter_AddRefs(securityInfo));
if (NS_FAILED(rv)) return rv;
- if (mUseNative && securityInfo) {
+ if (useNative && securityInfo) {
nsCOMPtr<nsIX509Cert> cert;
rv = securityInfo->GetServerCert(getter_AddRefs(cert));
if (NS_FAILED(rv)) return rv;
diff --git a/netwerk/protocol/http/nsHttpNTLMAuth.h b/netwerk/protocol/http/nsHttpNTLMAuth.h
index bab7bf3c02c..152ad5fce82 100644
--- a/netwerk/protocol/http/nsHttpNTLMAuth.h
+++ b/netwerk/protocol/http/nsHttpNTLMAuth.h
@@ -23,14 +23,6 @@ class nsHttpNTLMAuth : public nsIHttpAuthenticator {
private:
virtual ~nsHttpNTLMAuth() = default;
- // This flag indicates whether we are using the native NTLM implementation
- // or the internal one.
- bool mUseNative{false};
-
- // Whether the prefs let this host use the logged-in user's identity. Set by
- // ChallengeReceived, read by GenerateCredentials.
- bool mAllowDefaultCredentials{false};
-
static StaticRefPtr<nsHttpNTLMAuth> gSingleton;
};