Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect reference resolution in Passwords
DescriptionIncorrect reference resolution in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker513786555
Fix commite7ea3a7cbd14 (chromium/src) +55/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc
  • chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
  • components/password_manager/core/browser/password_suggestion_generator.cc
  • components/password_manager/core/browser/password_suggestion_generator_unittest.cc
From e7ea3a7cbd141487f2b62d3b820ae00c96ccec1d Mon Sep 17 00:00:00 2001
From: Viktor Semeniuk <vsemeniuk@google.com>
Date: Wed, 01 Jul 2026 06:08:17 -0700
Subject: [PATCH] Verify scheme is https before returning any favicons

Fixed: 513786555
Change-Id: I8739b0143e34f2253a44c011c5ce4c912c409a37
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8024861
Reviewed-by: Mohamed Amir Yosef <mamir@chromium.org>
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Cr-Commit-Position: refs/heads/main@{#1655445}
---

diff --git a/chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc b/chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc
index 817e26e..548db319 100644
--- a/chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc
+++ b/chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc
@@ -15,6 +15,7 @@
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "ui/gfx/image/image.h"
 #include "url/gurl.h"
+#include "url/url_constants.h"
 
 namespace autofill {
 namespace {
@@ -69,6 +70,11 @@
     base::CancelableTaskTracker* task_tracker,
     OnLoadSuccess on_success,
     OnLoadFail on_fail) {
+  if (!favicon_details.domain_url.SchemeIs(url::kHttpsScheme)) {
+    std::move(on_fail).Run();
+    return;
+  }
+
   auto cached_image_it = cache_.Get(favicon_details.domain_url);
   if (cached_image_it != cache_.end()) {
     std::move(on_success).Run(cached_image_it->second);
diff --git a/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc b/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
index 4659ebc..1e715de 100644
--- a/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
@@ -319,5 +319,36 @@
       /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
 }
 
+TEST_F(PasswordFaviconLoaderTest, FailsForNonHttpsDomainUrlOnFaviconService) {
+  EXPECT_CALL(large_icon_service(), GetLargeIconFromCacheFallbackToGoogleServer)
+      .Times(0);
+
+  base::MockOnceCallback<void(const gfx::Image&)> on_success;
+  base::MockOnceClosure on_fail;
+
+  EXPECT_CALL(on_success, Run).Times(0);
+  EXPECT_CALL(on_fail, Run);
+
+  loader().Load(
+      Suggestion::FaviconDetails(/*domain_url=*/GURL("http://google.com"),
+                                 /*can_be_requested_from_google=*/true),
+      /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
+}
+
+TEST_F(PasswordFaviconLoaderTest, FailsForNonHttpsDomainUrlOnImageFetcher) {
+  EXPECT_CALL(image_fetcher(), FetchImageAndData_).Times(0);
+
+  base::MockOnceCallback<void(const gfx::Image&)> on_success;
+  base::MockOnceClosure on_fail;
+
+  EXPECT_CALL(on_success, Run).Times(0);
+  EXPECT_CALL(on_fail, Run);
+
+  loader().Load(
+      Suggestion::FaviconDetails(/*domain_url=*/GURL("http://google.com"),
+                                 /*can_be_requested_from_google=*/false),
+      /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
+}
+
 }  // namespace
 }  // namespace autofill
diff --git a/components/password_manager/core/browser/password_suggestion_generator.cc b/components/password_manager/core/browser/password_suggestion_generator.cc
index dc44e6e9..a562372 100644
--- a/components/password_manager/core/browser/password_suggestion_generator.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator.cc
@@ -41,6 +41,7 @@
 #include "google_apis/gaia/gaia_auth_util.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "url/gurl.h"
+#include "url/url_constants.h"
 
 namespace password_manager {
 
@@ -301,7 +302,8 @@
                                    ? Suggestion::Acceptability::kAcceptable
                                    : Suggestion::Acceptability::kUnacceptable;
     if (FacetURI::FromPotentiallyInvalidSpec(domain_info.signon_realm)
-            .IsValidWebFacetURI()) {
+            .IsValidWebFacetURI() &&
+        domain_info.url.SchemeIs(url::kHttpsScheme)) {
       suggestion.custom_icon = Suggestion::FaviconDetails(
           domain_info.url, favicon_can_be_requested_from_google);
     }
diff --git a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
index 5d4ee4c..b40695dd 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -1591,6 +1591,21 @@
   EXPECT_THAT(suggestions[0], Not(FaviconCanBeRequestedFromGoogle()));
 }
 
+TEST_F(PasswordSuggestionGeneratorTest,
+       ManualFallback_Favicons_NoFaviconDetailsForNonHttpsUrl) {
+  PasswordForm form =
+      CreateEntry("user@example.com", "pass", GURL("http://127.0.0.1:8080/"),
+                  PasswordForm::MatchType::kExact);
+  form.signon_realm = "https://example.com/";
+
+  std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+      {form}, IsTriggeredOnPasswordForm(true));
+
+  ASSERT_GE(suggestions.size(), 1u);
+  EXPECT_FALSE(std::holds_alternative<Suggestion::FaviconDetails>(
+      suggestions[0].custom_icon));
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
 
 #if BUILDFLAG(ENABLE_DICE_SUPPORT)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc b/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
index 4659ebc..1e715de 100644
--- a/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
+++ b/chrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
@@ -319,5 +319,36 @@
       /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
 }
 
+TEST_F(PasswordFaviconLoaderTest, FailsForNonHttpsDomainUrlOnFaviconService) {
+  EXPECT_CALL(large_icon_service(), GetLargeIconFromCacheFallbackToGoogleServer)
+      .Times(0);
+
+  base::MockOnceCallback<void(const gfx::Image&)> on_success;
+  base::MockOnceClosure on_fail;
+
+  EXPECT_CALL(on_success, Run).Times(0);
+  EXPECT_CALL(on_fail, Run);
+
+  loader().Load(
+      Suggestion::FaviconDetails(/*domain_url=*/GURL("http://google.com"),
+                                 /*can_be_requested_from_google=*/true),
+      /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
+}
+
+TEST_F(PasswordFaviconLoaderTest, FailsForNonHttpsDomainUrlOnImageFetcher) {
+  EXPECT_CALL(image_fetcher(), FetchImageAndData_).Times(0);
+
+  base::MockOnceCallback<void(const gfx::Image&)> on_success;
+  base::MockOnceClosure on_fail;
+
+  EXPECT_CALL(on_success, Run).Times(0);
+  EXPECT_CALL(on_fail, Run);
+
+  loader().Load(
+      Suggestion::FaviconDetails(/*domain_url=*/GURL("http://google.com"),
+                                 /*can_be_requested_from_google=*/false),
+      /*task_tracker=*/nullptr, on_success.Get(), on_fail.Get());
+}
+
 }  // namespace
 }  // namespace autofill
diff --git a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
index 5d4ee4c..b40695dd 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -1591,6 +1591,21 @@
   EXPECT_THAT(suggestions[0], Not(FaviconCanBeRequestedFromGoogle()));
 }
 
+TEST_F(PasswordSuggestionGeneratorTest,
+       ManualFallback_Favicons_NoFaviconDetailsForNonHttpsUrl) {
+  PasswordForm form =
+      CreateEntry("user@example.com", "pass", GURL("http://127.0.0.1:8080/"),
+                  PasswordForm::MatchType::kExact);
+  form.signon_realm = "https://example.com/";
+
+  std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+      {form}, IsTriggeredOnPasswordForm(true));
+
+  ASSERT_GE(suggestions.size(), 1u);
+  EXPECT_FALSE(std::holds_alternative<Suggestion::FaviconDetails>(
+      suggestions[0].custom_icon));
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
 
 #if BUILDFLAG(ENABLE_DICE_SUPPORT)
Loading diff…

Original Bug Report

reported by vm...@google.com

SSRF in Password Favicon Loader via signon_realm/URL mismatch and PNA bypass

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in the password manager’s suggestion generation allows a poisoned credential to trigger a blind SSRF. The generator validates the ‘signon_realm’ field but uses the independent ‘url’ field for favicon fetching, bypassing Private Network Access (PNA) protections by using the browser’s system network context.

Affected files:

  • components/password_manager/core/browser/password_suggestion_generator.cc
  • chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc
  • components/password_manager/core/browser/ui/credential_ui_entry.cc
  • components/password_manager/core/browser/password_store/login_database.cc
  • components/password_manager/core/browser/sync/password_proto_utils.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential logic flaw in the password manager’s manual-fallback suggestion generation allows a malicious password entry to trigger a blind Server-Side Request Forgery (SSRF) that bypasses Private Network Access (PNA) protections. This occurs because the code validates one field of the credential but uses a different, independent field as the target for a network request.

Root Cause Analysis

In components/password_manager/core/browser/password_suggestion_generator.cc, the function AppendManualFallbackSuggestions determines whether to attach favicon details to a suggestion by checking if the signon_realm is a valid web facet (requiring an HTTPS scheme). However, it uses the independent domain_info.url field as the target for the fetch:

if (FacetURI::FromPotentiallyInvalidSpec(domain_info.signon_realm).IsValidWebFacetURI()) {
  suggestion.custom_icon = Suggestion::FaviconDetails(
      domain_info.url, favicon_can_be_requested_from_google);
}

In the password store (and the Chrome Sync protocol), signon_realm and url (origin) are independent fields. An attacker who can influence the password store (e.g., via Chrome Sync from a compromised device, or via Google-Family password sharing) can create an entry where signon_realm is a valid HTTPS URL (e.g., https://attacker.com), while the origin url targets an arbitrary internal or local resource (e.g., http://127.0.0.1:8080/).

Vulnerability Details

When the victim triggers the manual-fallback password popup (e.g., via a right-click on a password field), these suggestions are generated. For users with a custom sync passphrase, or for local-only credentials, the browser attempts to fetch the favicon directly in chrome/browser/ui/views/autofill/popup/password_favicon_loader.cc:

if (!favicon_details.can_be_requested_from_google) {
  GURL::Replacements domain_url_replacements;
  domain_url_replacements.SetPathStr(kFaviconFilename);
  image_fetcher_->FetchImage(
      favicon_details.domain_url.ReplaceComponents(domain_url_replacements),
      ...);
}

The image_fetcher used here is initialized in chrome/browser/ui/views/autofill/popup/popup_view_views.cc using the SystemNetworkContextManager::GetSharedURLLoaderFactory(). Requests issued from the system network context lack a web initiator and a client_security_state.

Inside the network service, the LocalNetworkAccessChecker allows requests that lack a client_security_state by default. This enables the browser to issue GET requests to internal network services (RFC1918), localhost, or cloud metadata services, bypassing PNA enforcement.

Potential Attack Steps

  1. An attacker pushes a malicious password record to the victim’s account via Sync or Family Sharing. The record has a signon_realm of https://legit-site.com and an origin of http://127.0.0.1:8080/admin?cmd=shutdown.
  2. The victim, who has a custom sync passphrase or is using local-only storage, triggers the manual-fallback password menu on any page.
  3. The browser process generates the suggestion, passes the signon_realm check, and initiates an image fetch to the malicious internal URL.
  4. The network request is issued to the internal service, bypassing PNA checks. The path is forced to /favicon.ico, but attacker-controlled query parameters may be preserved.

Impact

This is a blind SSRF primitive. While the response body is processed by an out-of-process image decoder and not returned to the attacker, the ability to bypass PNA and reach internal services can be used for network probing or triggering side effects on internal endpoints that respond to GET requests.

Suggested Fix

Ensure that the url used for favicon fetching is derived from or validated against the signon_realm. Additionally, consider ensuring that favicon fetches are performed with an appropriate ClientSecurityState to enforce Private Network Access protections.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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