CVE-2026-11031
Overview
Files Changed
chrome/browser/sync/test/integration/password_sharing_invitation_helper.ccchrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cccomponents/password_manager/core/browser/sharing/password_receiver_service_impl.cccomponents/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
Patch
From ff624feb9479ff07b89ee0a9abf9dd490a7a66f7 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
Date: Tue, 14 Apr 2026 04:22:58 -0700
Subject: [PATCH] [PasswordSharing] The origin should match the signon realm
This adds a check to `IsValidSharedPasswordForm` to ensure that for
HTML-based passwords, the origin of the form's URL matches the origin of
the signon realm. This prevents accepting shared passwords where the
signon realm might be spoofed to a different origin.
Bug: 497748760
Change-Id: I0faf0856b29220a5057ff324301820280919a581
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7757181
Reviewed-by: Vasilii Sukhanov <vasilii@chromium.org>
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614365}
---
diff --git a/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc b/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
index 1b0576c8..365a173 100644
--- a/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
+++ b/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
@@ -16,7 +16,7 @@
namespace password_sharing_helper {
namespace {
-constexpr char kSignonRealm[] = "signon_realm";
+constexpr char kSignonRealm[] = "http://abc.com/";
constexpr char kOrigin[] = "http://abc.com/";
constexpr char kUsernameElement[] = "username_element";
constexpr char kPasswordElement[] = "password_element";
diff --git a/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc b/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
index 2598787d..615f90f 100644
--- a/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
@@ -38,7 +38,7 @@
constexpr char kRecipientUserId[] = "recipient_user_id";
constexpr char kPasswordValue[] = "password";
-constexpr char kSignonRealm[] = "signon_realm";
+constexpr char kSignonRealm[] = "http://abc.com/";
constexpr char kOrigin[] = "http://abc.com/";
constexpr char kUsernameElement[] = "username_element";
constexpr char kUsernameValue[] = "username";
diff --git a/components/password_manager/core/browser/sharing/password_receiver_service_impl.cc b/components/password_manager/core/browser/sharing/password_receiver_service_impl.cc
index b0dfae27..f8d3dc7 100644
--- a/components/password_manager/core/browser/sharing/password_receiver_service_impl.cc
+++ b/components/password_manager/core/browser/sharing/password_receiver_service_impl.cc
@@ -18,6 +18,7 @@
#include "components/password_manager/core/browser/sharing/incoming_password_sharing_invitation_sync_bridge.h"
#include "components/sync/model/data_type_controller_delegate.h"
#include "components/sync/service/sync_service.h"
+#include "url/origin.h"
namespace password_manager {
@@ -40,6 +41,13 @@
if (!form.url.is_valid() || form.url.is_empty()) {
return false;
}
+ if (form.scheme == PasswordForm::Scheme::kHtml &&
+ form.url.SchemeIsHTTPOrHTTPS()) {
+ if (url::Origin::Create(form.url) !=
+ url::Origin::Create(GURL(form.signon_realm))) {
+ return false;
+ }
+ }
if (!IsValidString16(form.username_element) ||
!IsValidString16(form.username_value) ||
!IsValidString16(form.password_element)) {
diff --git a/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc b/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
index 3e348121..2393955 100644
--- a/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
+++ b/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
@@ -671,4 +671,27 @@
1);
}
+TEST_F(PasswordReceiverServiceImplTest,
+ ShouldIgnoreInvitationWithMismatchedOriginAndSignonRealm) {
+ base::HistogramTester histogram_tester;
+ sync_pb::IncomingPasswordSharingInvitationSpecifics invitation =
+ CreateIncomingSharingInvitation();
+
+ invitation.mutable_client_only_unencrypted_data()
+ ->mutable_password_group_data()
+ ->mutable_element_data(0)
+ ->set_signon_realm("https://malicious.com/");
+
+ password_receiver_service()->ProcessIncomingSharingInvitation(invitation);
+
+ EXPECT_THAT(expected_password_store_for_syncing().stored_passwords(),
+ IsEmpty());
+
+ histogram_tester.ExpectUniqueSample(
+ "PasswordManager.ProcessIncomingPasswordSharingInvitationResult",
+ metrics_util::ProcessIncomingPasswordSharingInvitationResult::
+ kInvalidInvitation,
+ 1);
+}
+
} // namespace password_manager
Regression Test / PoC
diff --git a/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc b/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
index 1b0576c8..365a173 100644
--- a/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
+++ b/chrome/browser/sync/test/integration/password_sharing_invitation_helper.cc
@@ -16,7 +16,7 @@
namespace password_sharing_helper {
namespace {
-constexpr char kSignonRealm[] = "signon_realm";
+constexpr char kSignonRealm[] = "http://abc.com/";
constexpr char kOrigin[] = "http://abc.com/";
constexpr char kUsernameElement[] = "username_element";
constexpr char kPasswordElement[] = "password_element";
diff --git a/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc b/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
index 2598787d..615f90f 100644
--- a/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_outgoing_password_sharing_invitation_test.cc
@@ -38,7 +38,7 @@
constexpr char kRecipientUserId[] = "recipient_user_id";
constexpr char kPasswordValue[] = "password";
-constexpr char kSignonRealm[] = "signon_realm";
+constexpr char kSignonRealm[] = "http://abc.com/";
constexpr char kOrigin[] = "http://abc.com/";
constexpr char kUsernameElement[] = "username_element";
constexpr char kUsernameValue[] = "username";
diff --git a/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc b/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
index 3e348121..2393955 100644
--- a/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
+++ b/components/password_manager/core/browser/sharing/password_receiver_service_impl_unittest.cc
@@ -671,4 +671,27 @@
1);
}
+TEST_F(PasswordReceiverServiceImplTest,
+ ShouldIgnoreInvitationWithMismatchedOriginAndSignonRealm) {
+ base::HistogramTester histogram_tester;
+ sync_pb::IncomingPasswordSharingInvitationSpecifics invitation =
+ CreateIncomingSharingInvitation();
+
+ invitation.mutable_client_only_unencrypted_data()
+ ->mutable_password_group_data()
+ ->mutable_element_data(0)
+ ->set_signon_realm("https://malicious.com/");
+
+ password_receiver_service()->ProcessIncomingSharingInvitation(invitation);
+
+ EXPECT_THAT(expected_password_store_for_syncing().stored_passwords(),
+ IsEmpty());
+
+ histogram_tester.ExpectUniqueSample(
+ "PasswordManager.ProcessIncomingPasswordSharingInvitationResult",
+ metrics_util::ProcessIncomingPasswordSharingInvitationResult::
+ kInvalidInvitation,
+ 1);
+}
+
} // namespace password_manager
Original Bug Report
Potential Credential Injection via Origin Mismatch in Password Sharing
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The Password Manager sharing service does not validate that the origin and signon_realm fields in incoming invitations match. This potentially allows a malicious sender to spoof the display domain in the settings UI while injecting a credential that will autofill on a different, sensitive domain.
Affected files:
components/password_manager/core/browser/sharing/password_receiver_service_impl.cccomponents/password_manager/core/browser/ui/credential_ui_entry.cccomponents/password_manager/core/browser/password_store/psl_matching_helper.cccomponents/password_manager/core/browser/password_form.h
Estimated timestamp from git blame: 2024-03-05
Description
A potential logic flaw exists in Chrome’s Password Manager sharing feature. When a user receives a shared password invitation via Sync, PasswordReceiverServiceImpl::ProcessIncomingSharingInvitation converts the incoming protobuf into a PasswordForm object. The form.url and form.signon_realm fields are populated independently from attacker-controlled values in the invitation (origin and signon_realm respectively) without any cross-validation to ensure they represent the same or affiliated domains.
This discrepancy can be exploited in two ways:
- UI Spoofing: The Password Manager settings UI (
chrome://password-manager/) constructs aCredentialUIEntryand determines the display name by callingGetShownOrigin(). For web credentials, this function relies onform.url. An attacker can setoriginto a harmless-looking domain (e.g.,familyphotos.example) to evade manual review by the victim. - Targeted Autofill Injection: Chrome’s autofill engine determines which stored credentials match the current page by evaluating the
signon_realminGetMatchResult(). An attacker can setsignon_realmto a high-value target (e.g.,https://accounts.google.com/).
If the victim doesn’t already have a credential for the attacker’s chosen username on the target signon_realm, the invitation is auto-approved and saved. Furthermore, the skip_zero_click flag defaults to false and is not modified, making the injected credential eligible for silent Auto-SignIn via the Credential Management API.
Potential Reproduction Steps
Note: Our tooling agent does not have the ability to run code or construct Sync protobufs, so these steps are theoretical based on code analysis.
- An attacker, who shares a Google Family group with the victim, crafts a malicious
IncomingPasswordSharingInvitationSpecificsSync payload. - In
client_only_unencrypted_data().password_group_data().element_data(), the attacker setsorigintohttps://innocuous.example/andsignon_realmtohttps://accounts.google.com/. - The attacker sets the username to an account they control and provides a password.
- The invitation is sent to the victim via Sync.
- In
components/password_manager/core/browser/sharing/password_receiver_service_impl.cc,IncomingSharingInvitationToPasswordFormspopulatesform.urlandform.signon_realmdirectly from the payload. IsValidSharedPasswordFormchecks both strings for validity and length, but does not cross-validate them. It returnstrue.- Since the victim has no existing credential for that username on
accounts.google.com, the credential is auto-approved and saved to the local PasswordStore. - In the Password Manager UI, the victim sees a new entry for
innocuous.example. - When the victim visits
accounts.google.com, the malicious credential is offered for autofill (or automatically signed in if the API is used), potentially logging the victim into the attacker’s account.
Suggested Fix
In IsValidSharedPasswordForm (or IncomingSharingInvitationToPasswordForms), add validation to ensure that form.url and form.signon_realm are consistent. For standard web credentials, the origin derived from form.url should match the origin of form.signon_realm, or they should be verified as affiliated domains.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.