Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in CredentialProvider
DescriptionMemory corruption in CredentialProvider
ComponentCredentialProvider
Sub-componentEnterprise>CredentialProvider
Bug ClassMemory Corruption
Tracker502085234
Fix commit1eb310b5f4b1 (chromium/src) +130/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/credential_provider/gaiacp/os_user_manager.cc
modified
sid
chrome/credential_provider/test/gcp_fakes.cc
modified
flags
chrome/credential_provider/test/gcp_fakes.cc
modified

Files Changed

  • chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
  • chrome/credential_provider/gaiacp/os_user_manager.cc
  • chrome/credential_provider/test/gcp_fakes.cc
  • chrome/credential_provider/test/gcp_fakes.h
  • chrome/credential_provider/gaiacp/os_user_manager.h
From 1eb310b5f4b1714e027dc1defc5fee0136d8b80f Mon Sep 17 00:00:00 2001
From: Krishna Kurapati <[email protected]>
Date: Tue, 22 Sep 2026 11:01:21 -0700
Subject: [PATCH] make sure existing user flags are not changed when linking accounts

when linking Google account to an Windows user profile, we are changing user flags with direct assignment. This may override some flags unintentionally.

Bug: 502085234
Change-Id: Ib0bcbd677c145da1b60001569d88a3e731988ecb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8423217
Reviewed-by: Yusuf Sengul <[email protected]>
Reviewed-by: Tien Mai <[email protected]>
Commit-Queue: Krishna Kurapati <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1702788}
---
diff --git a/chrome/credential_provider/gaiacp/os_user_manager.cc b/chrome/credential_provider/gaiacp/os_user_manager.cc
index 5aee0b0..6084470 100644
--- a/chrome/credential_provider/gaiacp/os_user_manager.cc
+++ b/chrome/credential_provider/gaiacp/os_user_manager.cc
@@ -393,14 +393,30 @@
   return HRESULT_FROM_WIN32(NERR_UserExists);
 }
 
-HRESULT OSUserManager::SetDefaultPasswordChangePolicies(
-    const wchar_t* domain,
-    const wchar_t* username) {
+HRESULT OSUserManager::GetUserFlags(const wchar_t* domain,
+                                    const wchar_t* username,
+                                    DWORD* flags) {
+  DCHECK(flags);
+  LPBYTE buffer = nullptr;
+  NET_API_STATUS nsts = ::NetUserGetInfo(domain, username, 1, &buffer);
+  if (nsts != NERR_Success) {
+    LOGFN(ERROR) << "NetUserGetInfo(get user flags) nsts=" << nsts;
+    return HRESULT_FROM_WIN32(nsts);
+  }
+
+  USER_INFO_1* user_info = reinterpret_cast<USER_INFO_1*>(buffer);
+  *flags = user_info->usri1_flags;
+  ::NetApiBufferFree(buffer);
+  return S_OK;
+}
+
+HRESULT OSUserManager::SetUserFlags(const wchar_t* domain,
+                                    const wchar_t* username,
+                                    DWORD flags) {
   USER_INFO_1008 info1008;
   DWORD error;
   memset(&info1008, 0, sizeof(info1008));
-  info1008.usri1008_flags =
-      UF_PASSWD_CANT_CHANGE | UF_DONT_EXPIRE_PASSWD | UF_NORMAL_ACCOUNT;
+  info1008.usri1008_flags = flags;
   NET_API_STATUS nsts = ::NetUserSetInfo(
       domain, username, 1008, reinterpret_cast<LPBYTE>(&info1008), &error);
   if (nsts != NERR_Success) {
@@ -409,6 +425,20 @@
   return HRESULT_FROM_WIN32(nsts);
 }
 
+HRESULT OSUserManager::SetDefaultPasswordChangePolicies(
+    const wchar_t* domain,
+    const wchar_t* username) {
+  DWORD existing_flags = 0;
+  HRESULT hr = GetUserFlags(domain, username, &existing_flags);
+  if (FAILED(hr)) {
+    return hr;
+  }
+
+  DWORD new_flags = existing_flags | UF_PASSWD_CANT_CHANGE |
+                    UF_DONT_EXPIRE_PASSWD | UF_NORMAL_ACCOUNT;
+  return SetUserFlags(domain, username, new_flags);
+}
+
 HRESULT OSUserManager::ChangeUserPassword(const wchar_t* domain,
                                           const wchar_t* username,
                                           const wchar_t* old_password,
diff --git a/chrome/credential_provider/gaiacp/os_user_manager.h b/chrome/credential_provider/gaiacp/os_user_manager.h
index d5ccaa22..4d2cfe41 100644
--- a/chrome/credential_provider/gaiacp/os_user_manager.h
+++ b/chrome/credential_provider/gaiacp/os_user_manager.h
@@ -141,6 +141,17 @@
 
   // Returns the storage used for the instance pointer.
   static OSUserManager** GetInstanceStorage();
+
+ private:
+  // Gets the account control flags (usri1_flags) of the user.
+  virtual HRESULT GetUserFlags(const wchar_t* domain,
+                               const wchar_t* username,
+                               DWORD* flags);
+
+  // Sets the account control flags (usri1008_flags) of the user.
+  virtual HRESULT SetUserFlags(const wchar_t* domain,
+                               const wchar_t* username,
+                               DWORD flags);
 };
 
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc b/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
index dfc22639..20f1fbd 100644
--- a/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
+++ b/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
@@ -4,6 +4,10 @@
 
 #include "chrome/credential_provider/gaiacp/os_gaia_user_manager.h"
 
+#include <windows.h>
+
+#include <lm.h>
+
 #include "base/win/ntsecapi_shim.h"
 #include "base/win/scoped_bstr.h"
 #include "chrome/credential_provider/common/gcp_strings.h"
@@ -140,5 +144,33 @@
   EXPECT_STREQ(first_password, second_password);
 }
 
+TEST_F(GcpOSGaiaUserManagerTest,
+       SetDefaultPasswordChangePoliciesPreservesAccountDisableFlag) {
+  base::win::ScopedBstr sid;
+  DWORD error = 0;
+  std::wstring domain = OSUserManager::GetLocalDomain();
+  std::wstring username = L"disabled_user";
+  ASSERT_EQ(S_OK, fake_os_user_manager()->AddUser(
+                      username.c_str(), L"password", L"fullname", L"comment",
+                      true, domain.c_str(), sid.Receive(), &error));
+
+  // Simulate an account that has been disabled and does not currently have the
+  // password change policy flags set.
+  ASSERT_EQ(S_OK, fake_os_user_manager()->SetUserFlagsFake(
+                      domain.c_str(), username.c_str(),
+                      UF_ACCOUNTDISABLE | UF_NORMAL_ACCOUNT));
+
+  ASSERT_EQ(S_OK, OSUserManager::Get()->SetDefaultPasswordChangePolicies(
+                      domain.c_str(), username.c_str()));
+
+  DWORD flags = 0;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->GetUserFlagsFake(
+                      domain.c_str(), username.c_str(), &flags));
+  EXPECT_NE(0u, flags & UF_ACCOUNTDISABLE);
+  EXPECT_NE(0u, flags & UF_PASSWD_CANT_CHANGE);
+  EXPECT_NE(0u, flags & UF_DONT_EXPIRE_PASSWD);
+  EXPECT_NE(0u, flags & UF_NORMAL_ACCOUNT);
+}
+
 }  // namespace testing
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/test/gcp_fakes.cc b/chrome/credential_provider/test/gcp_fakes.cc
index 231592d..5f5e773 100644
--- a/chrome/credential_provider/test/gcp_fakes.cc
+++ b/chrome/credential_provider/test/gcp_fakes.cc
@@ -480,10 +480,39 @@
   return S_OK;
 }
 
-HRESULT FakeOSUserManager::SetDefaultPasswordChangePolicies(
-    const wchar_t* domain,
-    const wchar_t* username) {
-  return S_OK;
+HRESULT FakeOSUserManager::GetUserFlagsFake(const wchar_t* domain,
+                                            const wchar_t* username,
+                                            DWORD* flags) {
+  return GetUserFlags(domain, username, flags);
+}
+
+HRESULT FakeOSUserManager::SetUserFlagsFake(const wchar_t* domain,
+                                            const wchar_t* username,
+                                            DWORD flags) {
+  return SetUserFlags(domain, username, flags);
+}
+
+HRESULT FakeOSUserManager::GetUserFlags(const wchar_t* domain,
+                                        const wchar_t* username,
+                                        DWORD* flags) {
+  DCHECK(flags);
+  if (auto it = username_to_info_.find(username);
+      it != username_to_info_.end()) {
+    *flags = it->second.flags;
+    return S_OK;
+  }
+  return HRESULT_FROM_WIN32(NERR_UserNotFound);
+}
+
+HRESULT FakeOSUserManager::SetUserFlags(const wchar_t* domain,
+                                        const wchar_t* username,
+                                        DWORD flags) {
+  if (auto it = username_to_info_.find(username);
+      it != username_to_info_.end()) {
+    it->second.flags = flags;
+    return S_OK;
+  }
+  return HRESULT_FROM_WIN32(NERR_UserNotFound);
 }
 
 FakeOSUserManager::UserInfo::UserInfo(const wchar_t* domain,
@@ -495,7 +524,9 @@
       password(password),
       fullname(fullname),
       comment(comment),
-      sid(sid) {}
+      sid(sid),
+      flags(UF_PASSWD_CANT_CHANGE | UF_DONT_EXPIRE_PASSWD | UF_NORMAL_ACCOUNT) {
+}
 
 FakeOSUserManager::UserInfo::UserInfo() = default;
 
diff --git a/chrome/credential_provider/test/gcp_fakes.h b/chrome/credential_provider/test/gcp_fakes.h
index 0a1ed70..cdf0bba3 100644
--- a/chrome/credential_provider/test/gcp_fakes.h
+++ b/chrome/credential_provider/test/gcp_fakes.h
@@ -169,8 +169,13 @@
                                          const wchar_t* username,
                                          bool allow) override;
 
-  HRESULT SetDefaultPasswordChangePolicies(const wchar_t* domain,
-                                           const wchar_t* username) override;
+  HRESULT GetUserFlagsFake(const wchar_t* domain,
+                           const wchar_t* username,
+                           DWORD* flags);
+
+  HRESULT SetUserFlagsFake(const wchar_t* domain,
+                           const wchar_t* username,
+                           DWORD flags);
 
   bool IsDeviceDomainJoined() override;
 
@@ -197,6 +202,7 @@
     std::wstring fullname;
     std::wstring comment;
     std::wstring sid;
+    DWORD flags = 0;
   };
   const UserInfo GetUserInfo(const wchar_t* username);
 
@@ -245,6 +251,14 @@
   void RestoreOperation(FAILEDOPERATIONS op) { failure_reasons_.erase(op); }
 
  private:
+  HRESULT GetUserFlags(const wchar_t* domain,
+                       const wchar_t* username,
+                       DWORD* flags) override;
+
+  HRESULT SetUserFlags(const wchar_t* domain,
+                       const wchar_t* username,
+                       DWORD flags) override;
+
   raw_ptr<OSUserManager> original_manager_;
   DWORD next_rid_ = 0;
   std::map<std::wstring, UserInfo> username_to_info_;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc b/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
index dfc22639..20f1fbd 100644
--- a/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
+++ b/chrome/credential_provider/gaiacp/os_gaia_user_manager_unittest.cc
@@ -4,6 +4,10 @@
 
 #include "chrome/credential_provider/gaiacp/os_gaia_user_manager.h"
 
+#include <windows.h>
+
+#include <lm.h>
+
 #include "base/win/ntsecapi_shim.h"
 #include "base/win/scoped_bstr.h"
 #include "chrome/credential_provider/common/gcp_strings.h"
@@ -140,5 +144,33 @@
   EXPECT_STREQ(first_password, second_password);
 }
 
+TEST_F(GcpOSGaiaUserManagerTest,
+       SetDefaultPasswordChangePoliciesPreservesAccountDisableFlag) {
+  base::win::ScopedBstr sid;
+  DWORD error = 0;
+  std::wstring domain = OSUserManager::GetLocalDomain();
+  std::wstring username = L"disabled_user";
+  ASSERT_EQ(S_OK, fake_os_user_manager()->AddUser(
+                      username.c_str(), L"password", L"fullname", L"comment",
+                      true, domain.c_str(), sid.Receive(), &error));
+
+  // Simulate an account that has been disabled and does not currently have the
+  // password change policy flags set.
+  ASSERT_EQ(S_OK, fake_os_user_manager()->SetUserFlagsFake(
+                      domain.c_str(), username.c_str(),
+                      UF_ACCOUNTDISABLE | UF_NORMAL_ACCOUNT));
+
+  ASSERT_EQ(S_OK, OSUserManager::Get()->SetDefaultPasswordChangePolicies(
+                      domain.c_str(), username.c_str()));
+
+  DWORD flags = 0;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->GetUserFlagsFake(
+                      domain.c_str(), username.c_str(), &flags));
+  EXPECT_NE(0u, flags & UF_ACCOUNTDISABLE);
+  EXPECT_NE(0u, flags & UF_PASSWD_CANT_CHANGE);
+  EXPECT_NE(0u, flags & UF_DONT_EXPIRE_PASSWD);
+  EXPECT_NE(0u, flags & UF_NORMAL_ACCOUNT);
+}
+
 }  // namespace testing
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/test/gcp_fakes.cc b/chrome/credential_provider/test/gcp_fakes.cc
index 231592d..5f5e773 100644
--- a/chrome/credential_provider/test/gcp_fakes.cc
+++ b/chrome/credential_provider/test/gcp_fakes.cc
@@ -480,10 +480,39 @@
   return S_OK;
 }
 
-HRESULT FakeOSUserManager::SetDefaultPasswordChangePolicies(
-    const wchar_t* domain,
-    const wchar_t* username) {
-  return S_OK;
+HRESULT FakeOSUserManager::GetUserFlagsFake(const wchar_t* domain,
+                                            const wchar_t* username,
+                                            DWORD* flags) {
+  return GetUserFlags(domain, username, flags);
+}
+
+HRESULT FakeOSUserManager::SetUserFlagsFake(const wchar_t* domain,
+                                            const wchar_t* username,
+                                            DWORD flags) {
+  return SetUserFlags(domain, username, flags);
+}
+
+HRESULT FakeOSUserManager::GetUserFlags(const wchar_t* domain,
+                                        const wchar_t* username,
+                                        DWORD* flags) {
+  DCHECK(flags);
+  if (auto it = username_to_info_.find(username);
+      it != username_to_info_.end()) {
+    *flags = it->second.flags;
+    return S_OK;
+  }
+  return HRESULT_FROM_WIN32(NERR_UserNotFound);
+}
+
+HRESULT FakeOSUserManager::SetUserFlags(const wchar_t* domain,
+                                        const wchar_t* username,
+                                        DWORD flags) {
+  if (auto it = username_to_info_.find(username);
+      it != username_to_info_.end()) {
+    it->second.flags = flags;
+    return S_OK;
+  }
+  return HRESULT_FROM_WIN32(NERR_UserNotFound);
 }
 
 FakeOSUserManager::UserInfo::UserInfo(const wchar_t* domain,
@@ -495,7 +524,9 @@
       password(password),
       fullname(fullname),
       comment(comment),
-      sid(sid) {}
+      sid(sid),
+      flags(UF_PASSWD_CANT_CHANGE | UF_DONT_EXPIRE_PASSWD | UF_NORMAL_ACCOUNT) {
+}
 
 FakeOSUserManager::UserInfo::UserInfo() = default;
diff --git a/chrome/credential_provider/test/gcp_fakes.h b/chrome/credential_provider/test/gcp_fakes.h
index 0a1ed70..cdf0bba3 100644
--- a/chrome/credential_provider/test/gcp_fakes.h
+++ b/chrome/credential_provider/test/gcp_fakes.h
@@ -169,8 +169,13 @@
                                          const wchar_t* username,
                                          bool allow) override;
 
-  HRESULT SetDefaultPasswordChangePolicies(const wchar_t* domain,
-                                           const wchar_t* username) override;
+  HRESULT GetUserFlagsFake(const wchar_t* domain,
+                           const wchar_t* username,
+                           DWORD* flags);
+
+  HRESULT SetUserFlagsFake(const wchar_t* domain,
+                           const wchar_t* username,
+                           DWORD flags);
 
   bool IsDeviceDomainJoined() override;
 
@@ -197,6 +202,7 @@
     std::wstring fullname;
     std::wstring comment;
     std::wstring sid;
+    DWORD flags = 0;
   };
   const UserInfo GetUserInfo(const wchar_t* username);
 
@@ -245,6 +251,14 @@
   void RestoreOperation(FAILEDOPERATIONS op) { failure_reasons_.erase(op); }
 
  private:
+  HRESULT GetUserFlags(const wchar_t* domain,
+                       const wchar_t* username,
+                       DWORD* flags) override;
+
+  HRESULT SetUserFlags(const wchar_t* domain,
+                       const wchar_t* username,
+                       DWORD flags) override;
+
   raw_ptr<OSUserManager> original_manager_;
   DWORD next_rid_ = 0;
   std::map<std::wstring, UserInfo> username_to_info_;
Loading diff…

Original Bug Report

reported by [email protected]

Local Privilege Escalation in GCPW via Unfiltered Account Mapping

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: The GCPW cloud-association feature fails to filter privileged local accounts when mapping Google Workspace users to Windows accounts. An attacker with Workspace directory privileges can map their account to the built-in Administrator; a secondary bug inadvertently enables the account, allowing a password reset. This allows the attacker to escalate privileges to local SYSTEM on the enrolled machine.

Affected files:

  • chrome/credential_provider/gaiacp/gaia_credential_base.cc
  • chrome/credential_provider/gaiacp/os_user_manager.cc
  • chrome/credential_provider/gaiacp/gaia_credential.h

Estimated timestamp from git blame: 2024-01-16

Description

A potential Local Privilege Escalation (LPE) vulnerability exists in the Google Credential Provider for Windows (GCPW) cloud-association feature. It stems from a combination of unfiltered account mapping and a logic error that overwrites account flags.

When a Workspace user logs in and enable_cloud_association is active, GCPW retrieves the Enhanced_desktop_security.Local_Windows_accounts custom schema attribute to map the Google account to a local Windows account. The function OSUserManager::GetUserSID calls LookupAccountName to resolve this username to a SID. However, it performs no filtering to reject well-known privileged accounts (such as the built-in Administrator, RID 500).

Additionally, after a successful mapping, CGaiaCredentialBase::ValidateOrCreateUser calls OSUserManager::SetDefaultPasswordChangePolicies to enforce password rules. In this function, info1008.usri1008_flags is set using a direct assignment (=) rather than a bitwise OR (|=). This unconditionally clears existing flags, including UF_ACCOUNTDISABLE, inadvertently enabling the mapped account even if the OS had it disabled by default.

Finally, because the local Windows password does not match the Google password, GCPW displays the “Forgot password” flow. Submitting this flow calls OSUserManager::SetUserPassword, which uses NetUserSetInfo level 1003 to forcefully reset the local password without requiring the original password, running in the SYSTEM context of LogonUI.

Potential Steps to Reproduce

(Note: These are suggested steps based on static code analysis, as our tooling agent does not have the ability to execute code to verify this live).

  1. An attacker with Workspace User Management privileges (or a compromised admin account) sets the Enhanced_desktop_security.Local_Windows_accounts attribute for their user (e.g., [email protected]) to un:Administrator.
  2. The attacker initiates a login on a GCPW-enrolled Windows machine via the “Add user” tile.
  3. The attacker authenticates successfully with Google.
  4. GCPW maps the account to the local built-in Administrator SID and inadvertently enables the account by overwriting its flags in SetDefaultPasswordChangePolicies.
  5. GCPW prompts for the local Windows password. The attacker clicks the “Forgot password” link and submits the confirmation.
  6. GCPW force-resets the local Administrator password to the attacker’s Google password.
  7. The attacker is logged into the Windows desktop as the built-in Administrator, achieving local SYSTEM privileges.

Suggested Fix

  1. Filter Privileged SIDs: In OSUserManager::GetUserSID or CGaiaCredential::ValidateExistingUser, validate the resolved SID to explicitly reject mapping to well-known privileged RIDs (such as DOMAIN_USER_RID_ADMIN / 500) and ensure the SID_NAME_USE is appropriate for a standard user.
  2. Fix Flag Overwrite: In OSUserManager::SetDefaultPasswordChangePolicies, first retrieve the current account flags using NetUserGetInfo (e.g., level 1 or 4). Then, apply the new policies using a bitwise OR (|=) so that existing flags like UF_ACCOUNTDISABLE are preserved.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker