Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in CredentialProvider
DescriptionInteger overflow in CredentialProvider
ComponentCredentialProvider
Bug ClassInteger Overflow
Tracker498986406
Fix commit3ddddab4f6c9 (chromium/src) +35/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/credential_provider/gaiacp/gaia_credential_base.cc
modified
TEST_F
chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
modified

Files Changed

  • chrome/credential_provider/gaiacp/gaia_credential_base.cc
  • chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
From 3ddddab4f6c9ae11e160eabd10dba29c65c8b84c Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Tue, 07 Apr 2026 10:05:23 -0700
Subject: [PATCH] [GCPW] Fix integer wrap in domain extraction

An integer wrap in the email extraction logic allowed an attacker to
bypass domain restrictions. Supplying an email without an '@' character
caused the entire string to be treated as the domain.

This CL adds a check to ensure the '@' character is present before
attempting to extract the domain from the email address.

Fixed: 498986406
Change-Id: I0d907b9a79c7cb7ef6bbc6278ab7d29613c01500
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728224
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Ted Choc <tedchoc@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1610840}
---

diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base.cc b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
index 531e102..2030af53 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
@@ -2499,7 +2499,15 @@
     }
 
     const std::wstring email = GetDictString(*properties, kKeyEmail);
-    const std::wstring email_domain = email.substr(email.find(L"@") + 1);
+    size_t at_pos = email.find(L"@");
+    if (at_pos == std::wstring::npos) {
+      LOGFN(ERROR) << "Email " << email << " is invalid (missing '@').";
+      *status_text =
+          CGaiaCredentialBase::AllocErrorString(IDS_INVALID_EMAIL_DOMAIN_BASE);
+      SecurelyClearDictionaryValue(properties);
+      return E_FAIL;
+    }
+    const std::wstring email_domain = email.substr(at_pos + 1);
     const std::vector<std::wstring> allowed_domains = GetEmailDomainsList();
 
     if (!std::ranges::contains(allowed_domains, email_domain)) {
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
index 6f101382..7fa271ef 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
@@ -914,7 +914,9 @@
   ASSERT_EQ(S_OK, cred.As(&test));
 
   std::wstring email = L"user@test.com";
-  std::wstring email_domain = email.substr(email.find(L"@") + 1);
+  size_t at_pos = email.find(L"@");
+  std::wstring email_domain =
+      at_pos != std::wstring::npos ? email.substr(at_pos + 1) : L"";
 
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(base::WideToUTF8(email)));
 
@@ -949,6 +951,29 @@
                           L"other@test.com,user@test.com"),
         ::testing::Values(L"test.com", L"best.com", L"test.com,best.com")));
 
+TEST_F(GcpGaiaCredentialBaseTest, InvalidEmailMissingAt) {
+  ASSERT_EQ(S_OK,
+            SetGlobalFlagForTesting(L"domains_allowed_to_login", L"test.com"));
+
+  // Create provider and start logon.
+  Microsoft::WRL::ComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+  Microsoft::WRL::ComPtr<ITestCredential> test;
+  ASSERT_EQ(S_OK, cred.As(&test));
+
+  // Set an email that doesn't have an '@' but matches an allowed domain.
+  // This would have bypassed the domain check before the fix.
+  std::wstring email = L"test.com";
+  ASSERT_EQ(S_OK, test->SetGlsEmailAddress(base::WideToUTF8(email)));
+
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
+
+  // Logon process should fail because the email is invalid.
+  std::wstring expected_error_msg =
+      GetStringResource(IDS_INVALID_EMAIL_DOMAIN_BASE);
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, expected_error_msg));
+}
+
 TEST_F(GcpGaiaCredentialBaseTest, StripEmailTLD) {
   USES_CONVERSION;
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
index 6f101382..7fa271ef 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base_unittest.cc
@@ -914,7 +914,9 @@
   ASSERT_EQ(S_OK, cred.As(&test));
 
   std::wstring email = L"user@test.com";
-  std::wstring email_domain = email.substr(email.find(L"@") + 1);
+  size_t at_pos = email.find(L"@");
+  std::wstring email_domain =
+      at_pos != std::wstring::npos ? email.substr(at_pos + 1) : L"";
 
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(base::WideToUTF8(email)));
 
@@ -949,6 +951,29 @@
                           L"other@test.com,user@test.com"),
         ::testing::Values(L"test.com", L"best.com", L"test.com,best.com")));
 
+TEST_F(GcpGaiaCredentialBaseTest, InvalidEmailMissingAt) {
+  ASSERT_EQ(S_OK,
+            SetGlobalFlagForTesting(L"domains_allowed_to_login", L"test.com"));
+
+  // Create provider and start logon.
+  Microsoft::WRL::ComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+  Microsoft::WRL::ComPtr<ITestCredential> test;
+  ASSERT_EQ(S_OK, cred.As(&test));
+
+  // Set an email that doesn't have an '@' but matches an allowed domain.
+  // This would have bypassed the domain check before the fix.
+  std::wstring email = L"test.com";
+  ASSERT_EQ(S_OK, test->SetGlsEmailAddress(base::WideToUTF8(email)));
+
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
+
+  // Logon process should fail because the email is invalid.
+  std::wstring expected_error_msg =
+      GetStringResource(IDS_INVALID_EMAIL_DOMAIN_BASE);
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, expected_error_msg));
+}
+
 TEST_F(GcpGaiaCredentialBaseTest, StripEmailTLD) {
   USES_CONVERSION;
Loading diff…

Original Bug Report

reported by vm...@google.com

Integer wrap in GCPW email domain extraction leads to domain allowlist bypass and LPE

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 security team.

Overview: An integer wrap in the Google Credential Provider for Windows (GCPW) email extraction logic allows an attacker to bypass domain restrictions. Supplying an email without an ‘@’ character causes the entire string to be treated as the domain. When combined with insufficient error handling, this allows a compromised GCPW sign-in process to create a persistent local Windows account with an attacker-controlled password.

Affected files:

  • chrome/credential_provider/gaiacp/gaia_credential_base.cc
  • chrome/credential_provider/gaiacp/win_http_url_fetcher.cc
  • chrome/credential_provider/gaiacp/gcp_utils.cc
  • chrome/credential_provider/gaiacp/os_user_manager.cc

Estimated timestamp from git blame: 2026-01-25

Summary

An integer wrap vulnerability in the Google Credential Provider for Windows (GCPW) allows an attacker to bypass the domain allowlist check. This occurs because the code incorrectly extracts the domain from an email address when the ‘@’ character is missing. This bypass, combined with insufficient error handling in network responses and user lookup logic, allows an attacker who has compromised the restricted GCPW Logon Sign-in (GLS) process to create a new local Windows account with an attacker-controlled password, resulting in a sandbox escape and local privilege escalation (LPE).

Vulnerability Details

1. Integer Wrap in Domain Extraction

In CGaiaCredentialBase::OnUserAuthenticated (running as SYSTEM inside LogonUI.exe), the email domain is extracted from the GLS-supplied JSON using the following logic:

const std::wstring email = GetDictString(*properties, kKeyEmail);
const std::wstring email_domain = email.substr(email.find(L"@") + 1);

When the email string does not contain an ‘@’ character, email.find(L"@") returns std::wstring::npos (defined as static_cast<size_t>(-1)). Due to unsigned integer arithmetic, npos + 1 wraps to 0. Consequently, email.substr(0) is executed, which returns the entire email string as the email_domain.

2. Domain Allowlist Bypass

An attacker can provide a string that matches an entry in the domains_allowed_to_login list (e.g., “contoso.com”) as the email field without including an ‘@’ character. The extraction logic will treat “contoso.com” as the domain, satisfying the allowlist check at gaia_credential_base.cc:2505:

if (!std::ranges::contains(allowed_domains, email_domain)) { ... }

3. Network and Lookup Error Mishandling

Following the bypass, ValidateOrCreateUser relies on the Google Admin SDK to fetch user information (FindExistingUserSidIfAvailable). The attacker provides a valid token for an unrelated account, allowing the initial token fetch to succeed. However, the subsequent Admin SDK query fails because “contoso.com” is not a valid user.

Crucially, the network and JSON parsing layers fail to handle the API error correctly:

  1. WinHttpUrlFetcher::Fetch ignores HTTP 4xx/5xx status codes and returns S_OK as long as it receives a response.
  2. SearchForListInStringDictUTF8 attempts to parse the error response. Finding the expected JSON paths missing, it gracefully does nothing and returns S_OK with an empty account list.
  3. FindExistingUserSidIfAvailable observes the empty lists and returns NTE_NOT_FOUND.
  4. In MakeUsernameForAccount (gaia_credential_base.cc:539), NTE_NOT_FOUND is not treated as a fatal error. Instead, the code falls back to the local user creation logic.

4. Attacker-Controlled Account Creation

Because the system falls back to new user creation, OSUserManager::Get()->CreateNewUser is invoked with a sanitized version of the fake email (e.g., contoso_com) and the password extracted from the attacker’s JSON (kKeyPassword). This silently creates a persistent local Windows user with known credentials, granting full interactive access.

Potential Exploitation Steps

(Note: These are potential steps as our tooling agent does not have code execution capabilities to verify a full PoC.)

  1. The attacker exploits a vulnerability in the WebView renderer running within the restricted gaia user context during the GCPW sign-in phase.
  2. From the compromised renderer, the attacker writes a malicious JSON payload to the stdout pipe connected to the elevated LogonUI.exe.
  3. The JSON payload specifies email: "contoso.com" (matching an allowed domain but lacking an @), alongside a chosen password and valid tokens obtained from an attacker-controlled Google account.
  4. GCPW parses the payload, bypasses the domain check due to the integer wrap, misinterprets the subsequent API failures as a user without existing mappings, and provisions a new Windows local account with the attacker’s password.
  5. The attacker gains interactive access to the machine, bypassing the sandbox and escalating privileges.

Suggested Fix

  1. Safely extract the email domain by ensuring an ‘@’ character is actually present:
size_t at_pos = email.find(L"@");
if (at_pos == std::wstring::npos) {
  // Handle error: invalid email format
  return E_FAIL;
}
const std::wstring email_domain = email.substr(at_pos + 1);
  1. Enforce HTTP status code validation in WinHttpUrlFetcher::Fetch to propagate API errors correctly.
  2. Ensure MakeUsernameForAccount correctly validates that the provided email string matches a valid email format before falling back to local account creation.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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