Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in CredentialProvider
DescriptionConfused deputy in CredentialProvider
ComponentCredentialProvider
Bug ClassLogic Error
Tracker498850269
Fix commit5a2545311a43 (chromium/src) +230/-129
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-01

Background

GCPW
Google Credential Provider for Windows, a Winlogon credential provider that authenticates users against Google accounts during Windows sign-in.
Confused deputy
a security flaw where a privileged component is tricked into misusing its authority on behalf of a less-privileged, attacker-influenced input.
`WinHttpUrlFetcher`
the credential provider’s HTTP client wrapper used here to download the account profile picture from a remote URL.
`picture_url`
the account avatar URL returned inside the GAIA sign-in payload, expected to point at a Google-owned host such as lh3.googleusercontent.com.

Root Cause Analysis

After a successful GCPW sign-in, credential_provider::UpdateProfilePictures in scoped_user_profile.cc downloaded the user’s Winlogon tile image by taking the picture_url from the sign-in response, appending a =s<size> suffix per entry in kProfilePictureSizes, and handing the result straight to WinHttpUrlFetcher::Create(GURL(current_picture_url)) for a fetch. The only checks performed were that GURL(...).GetPath() was longer than one character and that a fetcher object could be constructed; the code never validated the scheme or host of picture_url, so it did not enforce the invariant that this privileged, SYSTEM-context download only ever contacts a trusted Google image endpoint. Because the picture URL is attacker-influenceable data flowing into a high-privilege network fetch, the credential provider acted as a confused deputy, issuing requests to whatever host the payload named.

The fix (per the commit “GCPW: Add validation to picture URL”) introduces url/gurl.h and adds explicit URL validation before the download so that only well-formed, expected picture URLs are fetched. It also normalizes the image-size type from int to size_t across kProfilePictureSizes, GetUserSizedAccountPictureFilePath, and the registry helpers, removing signedness ambiguity in the sizes used to build paths and registry keys.

Key insight
The core mistake was treating the server-supplied picture_url as trusted and fetching it from a privileged Winlogon context without any scheme/host validation, turning the credential provider into a confused deputy. The fix constrains what may be downloaded by validating the picture URL before it reaches WinHttpUrlFetcher.

Attack Path

  1. Influence the sign-in payload An attacker who can shape the GAIA sign-in response (or otherwise control the returned picture_url field) supplies a URL pointing at a host of their choosing rather than a googleusercontent.com endpoint.
  2. Reach the download path On successful GCPW login, UpdateProfilePictures runs in the privileged credential-provider context and constructs current_picture_url from the attacker’s picture_url for each entry in kProfilePictureSizes.
  3. Trigger the privileged fetch The unvalidated URL is passed to WinHttpUrlFetcher::Create(GURL(current_picture_url)) and fetch, causing the SYSTEM-context deputy to issue an HTTP request to the attacker-named host.
  4. Exploit the deputy The attacker-directed request lets the privileged process contact endpoints it should never reach and pull attacker-controlled response bytes into the profile-picture handling logic.

Impact Assessment

An attacker who can influence the returned picture_url gains the ability to make the GCPW credential provider (a privileged Winlogon/SYSTEM-context component) send network requests to an attacker-chosen destination and consume its response as profile-picture data. The precondition is control or manipulation of the sign-in payload’s picture_url reaching UpdateProfilePictures after a GCPW login. Consistent with the metadata, this is a low-severity logic error (confused deputy) rather than direct memory-safety compromise.

Changed Functions

FunctionChangeNotes
for
chrome/credential_provider/gaiacp/scoped_user_profile.cc
modified
if
chrome/credential_provider/gaiacp/scoped_user_profile.cc
modified

Files Changed

  • chrome/credential_provider/gaiacp/reg_utils.cc
  • chrome/credential_provider/gaiacp/reg_utils.h
  • chrome/credential_provider/gaiacp/scoped_user_profile.cc

Audit Directions

  • Privileged fetch of remote-supplied URLs
    Audit any SYSTEM/Winlogon-context code that passes server-influenced URLs into WinHttpUrlFetcher or similar clients without scheme/host allowlisting.
  • Missing host validation on `GURL`
    Flag places that only check GURL::GetPath length or construct a fetcher but never assert the scheme and host against an expected trusted domain such as googleusercontent.com.
  • Signedness in size-derived paths and keys
    Review remaining int image/size values used to build file paths and registry keys, ensuring they use size_t to avoid signed/unsigned mismatches.
From 5a2545311a439fb9540df5c444a0f154259d16f1 Mon Sep 17 00:00:00 2001
From: Krishna Kurapati <krkurapati@google.com>
Date: Thu, 18 Jun 2026 10:17:08 -0700
Subject: [PATCH] GCPW: Add validation to picture URL

After successful login through GCPW, we download user's profile picture
to be shown in Winlon UI. Project Fortify bug recommends we validate the
picture url before downloading from it.

Typical picture url looks like:
https://lh3.googleusercontent.com/a/ACg8ocK_NQEh4PUNtaO75Cn6ddlzXe_UaAW9uQPSDW5t9R6FAnSj5Q=s96-c

Bug: 498850269
Change-Id: Ibf96fead6173cac58d0f79412ed7555c4262b868
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7952050
Reviewed-by: Tien Mai <tienmai@chromium.org>
Reviewed-by: Will Harris <wfh@chromium.org>
Commit-Queue: Krishna Kurapati <krkurapati@google.com>
Cr-Commit-Position: refs/heads/main@{#1649143}
---

diff --git a/chrome/credential_provider/gaiacp/reg_utils.cc b/chrome/credential_provider/gaiacp/reg_utils.cc
index 0aa41b6..eca23a5 100644
--- a/chrome/credential_provider/gaiacp/reg_utils.cc
+++ b/chrome/credential_provider/gaiacp/reg_utils.cc
@@ -124,7 +124,7 @@
   return S_OK;
 }
 
-std::wstring GetImageRegKeyForSpecificSize(int image_size) {
+std::wstring GetImageRegKeyForSpecificSize(size_t image_size) {
   return kImageRegKey + base::NumberToWString(image_size);
 }
 
@@ -268,7 +268,7 @@
 }
 
 HRESULT GetAccountPictureRegString(const std::wstring& user_sid,
-                                   int image_size,
+                                   size_t image_size,
                                    wchar_t* value,
                                    ULONG* length) {
   return GetMachineRegString(GetAccountPictureRegPathForUSer(user_sid),
@@ -278,7 +278,7 @@
 
 // Sets a specific account picture registry key in HKEY_LOCAL_MACHINE
 HRESULT SetAccountPictureRegString(const std::wstring& user_sid,
-                                   int image_size,
+                                   size_t image_size,
                                    const std::wstring& value) {
   return SetMachineRegString(GetAccountPictureRegPathForUSer(user_sid),
                              GetImageRegKeyForSpecificSize(image_size), value);
diff --git a/chrome/credential_provider/gaiacp/reg_utils.h b/chrome/credential_provider/gaiacp/reg_utils.h
index b4f7a08..4905a117 100644
--- a/chrome/credential_provider/gaiacp/reg_utils.h
+++ b/chrome/credential_provider/gaiacp/reg_utils.h
@@ -248,13 +248,13 @@
 
 // Gets a specific account picture registry key in HKEY_LOCAL_MACHINE
 HRESULT GetAccountPictureRegString(const std::wstring& user_sid,
-                                   int image_size,
+                                   size_t image_size,
                                    wchar_t* value,
                                    ULONG* length);
 
 // Sets a specific account picture registry key in HKEY_LOCAL_MACHINE
 HRESULT SetAccountPictureRegString(const std::wstring& user_sid,
-                                   int image_size,
+                                   size_t image_size,
                                    const std::wstring& value);
 
 // Retrieves an identifier that is stored under
diff --git a/chrome/credential_provider/gaiacp/scoped_user_profile.cc b/chrome/credential_provider/gaiacp/scoped_user_profile.cc
index 9c2038a..918e41e 100644
--- a/chrome/credential_provider/gaiacp/scoped_user_profile.cc
+++ b/chrome/credential_provider/gaiacp/scoped_user_profile.cc
@@ -39,6 +39,7 @@
 #include "chrome/credential_provider/gaiacp/logging.h"
 #include "chrome/credential_provider/gaiacp/reg_utils.h"
 #include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h"
+#include "url/gurl.h"
 
 namespace credential_provider {
 
@@ -51,7 +52,7 @@
 // retrying would not be needed, but this notification does not exist.
 const int kWaitForProfileCreationRetryCount = 30;
 
-constexpr int kProfilePictureSizes[] = {32, 40, 48, 96, 192, 240, 448};
+constexpr size_t kProfilePictureSizes[] = {32, 40, 48, 96, 192, 240, 448};
 
 std::string GetEncryptedRefreshToken(
     base::win::ScopedHandle::Handle logon_handle,
@@ -109,7 +110,7 @@
 
 base::FilePath GetUserSizedAccountPictureFilePath(
     const base::FilePath& account_picture_path,
-    int size,
+    size_t size,
     const std::wstring& picture_extension) {
   return account_picture_path.Append(
       base::StrCat({L"GoogleAccountPicture_", base::NumberToWString(size),
@@ -265,128 +266,6 @@
   return hr;
 }
 
-HRESULT UpdateProfilePictures(const std::wstring& sid,
-                              const std::wstring& picture_url,
-                              bool force_update) {
-  DCHECK(!sid.empty());
-  DCHECK(!picture_url.empty());
-
-  // Try to download profile pictures of all required sizes for windows.
-  // Needed profile picture sizes are in |kProfilePictureSizes|.
-  // The way Windows8+ stores profile pictures is the following:
-  // In |reg_utils.cc:kAccountPicturesRootRegKey| there is a registry key
-  // for each resolution of profile picture needed. The keys are names
-  // "Image[x]" where [x] is the resolution of the picture.
-  // Each key points to a profile picture of the correct resolution on disk.
-  // Generally the profile pictures are stored under:
-  // FOLDERID_PublicUserTiles\\{user sid}
-
-  std::wstring picture_url_path =
-      base::UTF8ToWide(GURL(base::AsStringPiece16(picture_url)).GetPath());
-  if (picture_url_path.size() <= 1) {
-    LOGFN(ERROR) << "Invalid picture url=" << picture_url;
-    return E_FAIL;
-  }
-
-  base::FilePath account_picture_path;
-  HRESULT hr = GetUserAccountPicturePath(sid, &account_picture_path);
-  if (FAILED(hr)) {
-    LOGFN(ERROR) << "Failed to get account picture known folder=" << putHR(hr);
-    return E_FAIL;
-  }
-
-  if (!base::PathExists(account_picture_path)) {
-    hr = CreateDirectoryWithRestrictedAccess(account_picture_path);
-    if (FAILED(hr)) {
-      LOGFN(ERROR) << "Failed to create profile picture directory="
-                   << account_picture_path << " hr=" << putHR(hr);
-      return hr;
-    }
-  }
-
-  std::wstring base_picture_extension = kDefaultProfilePictureFileExtension;
-
-  size_t last_period = picture_url_path.find_last_of('.');
-  if (last_period != std::string::npos)
-    base_picture_extension = picture_url_path.substr(last_period);
-
-  for (auto image_size : kProfilePictureSizes) {
-    base::FilePath target_picture_path = GetUserSizedAccountPictureFilePath(
-        account_picture_path, image_size, base_picture_extension);
-    bool needs_to_save_original =
-        force_update || !base::PathExists(target_picture_path);
-
-    // Skip if the file already exists and an update is not forced.
-    if (!needs_to_save_original) {
-      // Update the reg string for the image if it is not up to date.
-      wchar_t old_picture_path[MAX_PATH];
-      ULONG path_size = std::size(old_picture_path);
-      hr = GetAccountPictureRegString(sid, image_size, old_picture_path,
-                                      &path_size);
-      if (FAILED(hr) || target_picture_path.value() != old_picture_path) {
-        hr = SetAccountPictureRegString(sid, image_size,
-                                        target_picture_path.value());
-        if (FAILED(hr))
-          LOGFN(ERROR) << "SetAccountPictureRegString(pic) hr=" << putHR(hr);
-      }
-      continue;
-    }
-
-    std::size_t found = base::WideToUTF8(picture_url).rfind("=s");
-    std::string current_picture_url;
-    if (found != std::string::npos)
-      current_picture_url = base::WideToUTF8(picture_url).substr(0, found) +
-                            base::StringPrintf("=s%i", image_size);
-    else
-      // Fallback to default picture url if parsing fails.
-      current_picture_url = base::WideToUTF8(picture_url) +
-                            base::StringPrintf("=s%i", image_size);
-
-    auto fetcher = WinHttpUrlFetcher::Create(GURL(current_picture_url));
-    if (!fetcher) {
-      LOGFN(ERROR) << "Failed to create fetcher for=" << current_picture_url;
-      continue;
-    }
-
-    std::vector<char> response;
-    hr = fetcher->Fetch(&response);
-    if (FAILED(hr)) {
-      LOGFN(ERROR) << "fetcher.Fetch hr=" << putHR(hr);
-      continue;
-    }
-
-    if (needs_to_save_original) {
-      SaveProcessedProfilePictureToDisk(
-          target_picture_path, response,
-          base::BindOnce(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/credential_provider/gaiacp/scoped_user_profile_unittest.cc b/chrome/credential_provider/gaiacp/scoped_user_profile_unittest.cc
new file mode 100644
index 0000000..4f1057a0
--- /dev/null
+++ b/chrome/credential_provider/gaiacp/scoped_user_profile_unittest.cc
@@ -0,0 +1,58 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/credential_provider/gaiacp/scoped_user_profile.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace credential_provider {
+
+TEST(ScopedUserProfileStaticTest, IsValidPictureUrl) {
+  // Valid URLs
+  EXPECT_TRUE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://lh3.googleusercontent.com/a/abc"));
+  EXPECT_TRUE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://lh4.googleusercontent.com/a/abc=s96"));
+  EXPECT_TRUE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://googleusercontent.com/path"));
+
+  // Invalid hosts
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://malicious.com/picture.jpg"));
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://lh3.googleusercontent.com.malicious.com/a/abc"));
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://fake-googleusercontent.com/a/abc"));
+  EXPECT_FALSE(
+      ScopedUserProfile::IsValidPictureUrl(L"https://google.com/picture.jpg"));
+
+  // Invalid URLs
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(L"not-a-url"));
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(L""));
+
+  // Valid host but empty or root path
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://lh3.googleusercontent.com"));
+  EXPECT_FALSE(ScopedUserProfile::IsValidPictureUrl(
+      L"https://lh3.googleusercontent.com/"));
+}
+
+TEST(ScopedUserProfileStaticTest, BuildProfilePictureUrl) {
+  // URL without suffix
+  EXPECT_EQ("https://lh3.googleusercontent.com/a/abc=s96",
+            ScopedUserProfile::BuildProfilePictureUrl(
+                GURL("https://lh3.googleusercontent.com/a/abc"), 96));
+
+  // URL with existing suffix
+  EXPECT_EQ("https://lh3.googleusercontent.com/a/abc=s128",
+            ScopedUserProfile::BuildProfilePictureUrl(
+                GURL("https://lh3.googleusercontent.com/a/abc=s96"), 128));
+
+  // Invalid URL
+  EXPECT_EQ("",
+            ScopedUserProfile::BuildProfilePictureUrl(GURL("not-a-url"), 96));
+}
+
+}  // namespace credential_provider
diff --git a/chrome/credential_provider/test/BUILD.gn b/chrome/credential_provider/test/BUILD.gn
index e97bf001d..85bb13d 100644
--- a/chrome/credential_provider/test/BUILD.gn
+++ b/chrome/credential_provider/test/BUILD.gn
@@ -21,6 +21,7 @@
     "../gaiacp/gem_device_details_manager_unittest.cc",
     "../gaiacp/os_gaia_user_manager_unittest.cc",
     "../gaiacp/reauth_credential_unittest.cc",
+    "../gaiacp/scoped_user_profile_unittest.cc",
     "../gaiacp/user_policies_manager_unittest.cc",
     "../gaiacp/win_http_url_fetcher_unittest.cc",
     "com_fakes.cc",
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.