Low firefox Race 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionInformation disclosure due to race condition in the Networking component
ComponentNetworking
Bug ClassRace
Tracker2073584
Fix commit62eeacafabef (firefox) +42/-23
CISA KEVNot listed
CreditedAbdulrahman Alzahrani
Disclosed2026-10-27

Background

`nsHttpNTLMAuth`
Firefox’s NTLM HTTP authenticator. It is a process-wide singleton: the class holds static StaticRefPtr<nsHttpNTLMAuth> gSingleton and GetOrCreate() hands the same instance to every channel that negotiates NTLM.
`ChallengeReceived` / `GenerateCredentials`
The two authenticator entry points. ChallengeReceived is called when a server challenges with NTLM and decides policy (native vs. internal module, whether default credentials are allowed); GenerateCredentials later 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.

Key insight
Per-channel, security-critical NTLM policy (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

  1. 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).
  2. Race `ChallengeReceived` The approved host’s ChallengeReceived runs and sets the singleton’s mAllowDefaultCredentials = true, overwriting the false the untrusted host’s exchange had set.
  3. Untrusted exchange reaches `GenerateCredentials` For the attacker’s host, GenerateCredentials reads the stale mAllowDefaultCredentials == true, so the NS_ERROR_ABORT guard that normally blocks silent default-credential use is skipped.
  4. Silent SSO to attacker With an empty user/password, the native module authenticates as the logged-in OS user to the attacker’s host.
  5. 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

The code runs wherever HTTP auth is processed (the networking/parent process). No special privilege is needed beyond getting the victim’s browser to attempt NTLM against an attacker-controlled host while another approved NTLM exchange runs concurrently — a timing-dependent (racy) precondition, so exploitation is not deterministic. The gained asset is the victim’s default NTLM credentials (an NTLMv2 response usable for relay or offline cracking), which is a genuine information/credential disclosure, consistent with Mozilla’s low rating given the race precondition. This is a concurrency logic defect, not a memory-safety bug; no UAF/OOB is involved.

Changed Functions

FunctionChangeNotes
nsNTLMContinuationState
netwerk/protocol/http/nsHttpNTLMAuth.cpp
modified
if
netwerk/protocol/http/nsHttpNTLMAuth.cpp
modified

Files Changed

  • netwerk/protocol/http/nsHttpNTLMAuth.cpp
  • netwerk/protocol/http/nsHttpNTLMAuth.h

Audit Directions

  • State cached on singletons/statics
    Audit other nsIHttpAuthenticator implementations (e.g. Negotiate/Kerberos, SSPI) and any singleton that stores per-request decisions in member fields across ChallengeReceived→GenerateCredentials.
  • Cached policy vs. re-checked policy
    Look 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 misuse
    Review code that stores an nsIAuthModule or similar directly in *continuationState and later do_QueryInterfaces it, to ensure per-channel isolation and correct typing (the fix switched to a typed do_QueryObject).
  • Default-credential gates
    Trace 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.
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;
 };
 
Loading diff…