CVE-2026-79049
Overview
Background
- Password manual-fallback suggestions
- on desktop (the new test sits under
!BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)), the Autofill popup can list saved credentials, each with acustom_iconthat may hold aSuggestion::FaviconDetailsdescribing where to get the site’s favicon. - `Suggestion::FaviconDetails`
- a struct holding a
domain_urland acan_be_requested_from_googleflag; the popup’s favicon loader uses it to fetch an icon, either through the large-icon/favicon service (which can fall back to Google’s favicon server) or by fetching the image directly with an image fetcher. - `signon_realm` vs. `url`
- a stored
PasswordFormhas asignon_realm(the security realm the credential belongs to) and a separateurl(the page where it was saved). The two can differ: the new unit test pairssignon_realm = "https://example.com/"withurl = http://127.0.0.1:8080/. - `FacetURI::IsValidWebFacetURI`
- a validity check on the
signon_realmfacet. Before the patch, this was the only gate on whether a suggestion got favicon details.
Root Cause Analysis
In password_suggestion_generator.cc, the code that builds password suggestions attached Suggestion::FaviconDetails(domain_info.url, ...) whenever FacetURI::FromPotentiallyInvalidSpec(domain_info.signon_realm).IsValidWebFacetURI() was true. So the check ran on one field (signon_realm) but the icon was fetched from a different field (domain_info.url). The hunk does not show the name of the enclosing function. A credential with a valid HTTPS realm could therefore carry an arbitrary non-HTTPS url, such as plain http:// or a loopback/LAN address like http://127.0.0.1:8080/. The password popup’s favicon loader would then request a favicon for it with no scheme check. It did so either through the large-icon service, which can fall back to Google’s server, or with a direct image fetch.
The fix adds a check at both ends. The generator now requires domain_info.url.SchemeIs(url::kHttpsScheme) before attaching FaviconDetails. The loader’s Load method in password_favicon_loader.cc now calls on_fail and returns before the cache lookup and any network fetch when domain_url is not HTTPS. The new tests confirm that neither GetLargeIconFromCacheFallbackToGoogleServer nor FetchImageAndData_ is called for an http:// URL.
url, but only the credential’s signon_realm was validated, so the browser could fetch from a non-HTTPS (including local-network) address the realm check never covered. The fix requires an HTTPS scheme on the URL that is actually fetched, both when the suggestion is built and in the loader itself.Attack Path
- Credential with mismatched URL is stored
a saved credential ends up with an HTTPS
signon_realmbut a non-HTTPSurl, such ashttp://127.0.0.1:8080/(the unit-test shape). How an attacker would get such an entry into the store (for example password import, or a save from an HTTP page) is not shown by the patch (inferred). - User opens the password suggestion popup
the victim focuses a field and the desktop password suggestions or manual fallback are generated. Because the realm passes
IsValidWebFacetURI(), the suggestion receivesFaviconDetailsbuilt from the non-HTTPSurl. - Browser fetches the favicon from the unchecked URL
the popup’s favicon loader passes
domain_urlto the large-icon service or the image fetcher with no scheme check. This sends a browser-initiated request to a plaintext or local-network endpoint. - Consequence the request reveals to that endpoint, or to a network observer, that the user has a credential for the site. It also lets an attacker who controls the response decide which icon is shown next to the credential (inferred). It could additionally be used to probe local-network hosts (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/autofill/popup/password_favicon_loader.cccomponents/password_manager/core/browser/password_suggestion_generator_unittest.cccomponents/password_manager/core/browser/password_suggestion_generator.ccchrome/browser/ui/views/autofill/popup/password_favicon_loader_unittest.cc
Audit Directions
- Validate-one-field, use-anotherlook for code in the password manager and Autofill that validates
signon_realm(or an origin) but then usesPasswordForm::url,action, or an affiliation URL for network requests, display, or origin decisions. - Browser-initiated fetches from stored or synced datacheck other UI surfaces that fetch icons or images from URLs stored in user data (bookmarks, payment and Autofill profiles, affiliation data) for missing scheme checks, missing loopback/private-network checks, and Google-fallback leaks.
- Defense at the sinkmake sure fetchers like the favicon loader enforce their own URL policy (scheme, host class) rather than relying on every producer of
FaviconDetailsto filter correctly.
Patch
From e7ea3a7cbd141487f2b62d3b820ae00c96ccec1d Mon Sep 17 00:00:00 2001 From: Viktor Semeniuk <[email protected]> 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 <[email protected]> Commit-Queue: Viktor Semeniuk <[email protected]> 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/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/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("[email protected]", "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)
Regression Test / PoC
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("[email protected]", "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)
Original Bug Report
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.ccchrome/browser/ui/views/autofill/popup/password_favicon_loader.cccomponents/password_manager/core/browser/ui/credential_ui_entry.cccomponents/password_manager/core/browser/password_store/login_database.cccomponents/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
- An attacker pushes a malicious password record to the victim’s account via Sync or Family Sharing. The record has a
signon_realmofhttps://legit-site.comand anoriginofhttp://127.0.0.1:8080/admin?cmd=shutdown. - The victim, who has a custom sync passphrase or is using local-only storage, triggers the manual-fallback password menu on any page.
- The browser process generates the suggestion, passes the
signon_realmcheck, and initiates an image fetch to the malicious internal URL. - 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.