Chrome · Passwords
CVE-2026-87526
UAF in Passwords
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc |
modified | |
CreateUsernamePasswordWithEyeIconchrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc |
modified |
Files Changed
chrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc
Patch
From 6192ad9e4b4f24b9979c164e5aca27832ffa5273 Mon Sep 17 00:00:00 2001
From: Anna Tsvirchkova <atsvirchkova@google.com>
Date: Mon, 17 Aug 2026 03:16:50 -0700
Subject: [PATCH] Fix Use-after-free in SuccessfulPasswordChangeBubbleController
The use-after-free would be the following:
User->>View: 1. Clicks Eye Icon to reveal password
View->>OS: 2. Starts Async Re-Auth (passes callback with raw pointers)
User->>View: 3. Closes Bubble / Tab while Auth dialog is open
Note over View: 4. View & Labels destroyed (Heap memory FREED)
OS->>View: 5. Auth finishes & executes callback
Note over View: 6. Callback calls password_label->SetObscured()
The CL wraps handling the authentication result into a method, which is
passed to biometric auth via a WeakPtr. It also moves eye button click
handler into a method for conveniency.
Bug: 544484669
Change-Id: Ib6bb0c7e6ddda5b7ed07e0c381d799dc9460c130
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8261134
Commit-Queue: Anna Tsvirchkova <atsvirchkova@google.com>
Reviewed-by: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1680431}
---
diff --git a/chrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc b/chrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc
index a90fbbb..d0a36bd 100644
--- a/chrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc
+++ b/chrome/browser/ui/views/passwords/password_change/successful_password_change_view.cc
@@ -67,102 +67,6 @@
return view;
}
-// Row containing favicon, username, password and eye icon:
-// *--------------------------------------------------------*
-// | | Username | |
-// | Favicon |-----------------------------------| Eye icon |
-// | | Password | |
-// *--------------------------------------------------------*
-std::unique_ptr<views::View> CreateUsernamePasswordWithEyeIcon(
- SuccessfulPasswordChangeBubbleController* controller) {
- CHECK(controller);
-
- auto parent_view = std::make_unique<views::BoxLayoutView>();
- parent_view->SetInsideBorderInsets(ComputeRowMargins());
-
- // Set the same spacing as for RichHoverButton which is displayed bellow.
- const int icon_label_spacing = ChromeLayoutProvider::Get()->GetDistanceMetric(
- DISTANCE_RICH_HOVER_BUTTON_ICON_HORIZONTAL);
- parent_view->SetBetweenChildSpacing(icon_label_spacing);
-
- // Add favicon.
- views::ImageView* favicon_view =
- parent_view->AddChildView(std::make_unique<views::ImageView>());
- const int icon_size = GetLayoutConstant(LayoutConstant::kPageInfoIconSize);
- favicon_view->SetImageSize({icon_size, icon_size});
- favicon_view->SetImage(ui::ImageModel::FromVectorIcon(
- features::IsRoundedIconsEnabled() ? vector_icons::kGlobeIcon
- : vector_icons::kGlobeOldIcon,
- ui::kColorIcon, gfx::kFaviconSize));
- controller->RequestFavicon(base::BindOnce(
- [](views::ImageView* favicon_view, const gfx::Image& favicon) {
- if (!favicon.IsEmpty()) {
- favicon_view->SetImage(ui::ImageModel::FromImage(favicon));
- }
- },
- favicon_view));
-
- // Add username/password labels.
- auto* username_password_view =
- parent_view->AddChildView(CreateVerticalStackView());
- username_password_view->SetProperty(views::kBoxLayoutFlexKey,
- views::BoxLayoutFlexSpecification());
- username_password_view->AddChildView(
- CreateUsernameLabel(controller->GetUsername()));
- views::Label* password_label = username_password_view->AddChildView(
- CreatePasswordLabel(controller->GetNewPassword()));
-
- // Add eye icon which allows to reveal a password.
- auto* eye_icon = parent_view->AddChildView(
- CreateVectorToggleImageButton(views::Button::PressedCallback()));
- eye_icon->SetTooltipText(
- l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_SHOW_PASSWORD));
- eye_icon->SetToggledTooltipText(
- l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_HIDE_PASSWORD));
- eye_icon->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
- views::SetImageFromVectorIconWithColor(eye_icon,
- features::IsRoundedIconsEnabled()
- ? views::kVisibilityFilledIcon
- : views::kEyeOldIcon,
- {ui::kColorIcon, ui::kColorIcon});
- views::SetToggledImageFromVectorIconWithColor(
- eye_icon,
- features::IsRoundedIconsEnabled() ? views::kVisibilityOffFilledIcon
- : views::kEyeCrossedOldIcon,
- {ui::kColorIcon, ui::kColorIcon});
-
- base::RepeatingCallback<void(bool)> auth_result_callback =
- base::BindRepeating(
- [](views::ToggleImageButton* toggle_button,
- views::Label* password_label, bool auth_result) {
- if (!auth_result) {
- return;
- }
- password_label->SetObscured(!password_label->GetObscured());
- toggle_button->SetToggled(!toggle_button->GetToggled());
- },
- eye_icon, password_label);
-
- eye_icon->SetCallback(base::BindRepeating(
- [](base::WeakPtr<SuccessfulPasswordChangeBubbleController> controller,
- views::Label* password_label,
- base::RepeatingCallback<void(bool)> auth_callback) {
- if (!password_label->GetObscured()) {
- // Run callback to hide the password. No auth needed to do it.
- auth_callback.Run(true);
- return;
- }
- if (controller) {
- controller->AuthenticateUser(auth_callback);
- }
- },
- controller->GetWeakPtr(), password_label,
- std::move(auth_result_callback)));
- eye_icon->SetID(SuccessfulPasswordChangeView::kEyeIconButtonId);
-
- return parent_view;
-}
-
std::unique_ptr<views::View> CreateManagePasswordsView(
base::RepeatingClosure open_password_manager_closure) {
auto manage_passwords_button = std::make_unique<RichHoverButton>(
@@ -217,15 +121,14 @@
// width.
set_margins(gfx::Insets());
- views::View* username_password_row = root_view->AddChildView(
- CreateUsernamePasswordWithEyeIcon(controller_.get()));
+ root_view->AddChildView(CreateUsernamePasswordWithEyeIcon());
root_view->AddChildView(std::make_unique<views::Separator>());
root_view->AddChildView(CreateManagePasswordsView(base::BindRepeating(
&SuccessfulPasswordChangeBubbleController::OpenPasswordManager,
controller_->GetWeakPtr())));
SetShowIcon(true);
- SetInitiallyFocusedView(username_password_row->GetViewByID(kEyeIconButtonId));
+ SetInitiallyFocusedView(eye_icon_);
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
SetCloseCallback(base::BindRepeating(
[](SuccessfulPasswordChangeView* view) {
@@ -259,5 +162,85 @@
SetBubbleHeaderLottie(IDR_PASSWORD_CHANGE_SUCCESS_LOTTIE);
}
+std::unique_ptr<views::View>
+SuccessfulPasswordChangeView::CreateUsernamePasswordWithEyeIcon() {
+ auto parent_view = std::make_unique<views::BoxLayoutView>();
+ parent_view->SetInsideBorderInsets(ComputeRowMargins());
+
+ // Set the same spacing as for RichHoverButton which is displayed below.
+ const int icon_label_spacing = ChromeLayoutProvider::Get()->GetDistanceMetric(
+ DISTANCE_RICH_HOVER_BUTTON_ICON_HORIZONTAL);
+ parent_view->SetBetweenChildSpacing(icon_label_spacing);
+
+ // Add favicon.
+ views::ImageView* favicon_view =
+ parent_view->AddChildView(std::make_unique<views::ImageView>());
+ const int icon_size = GetLayoutConstant(LayoutConstant::kPageInfoIconSize);
+ favicon_view->SetImageSize({icon_size, icon_size});
+ favicon_view->SetImage(ui::ImageModel::FromVectorIcon(
+ features::IsRoundedIconsEnabled() ? vector_icons::kGlobeIcon
+ : vector_icons::kGlobeOldIcon,
+ ui::kColorIcon, gfx::kFaviconSize));
+ controller_->RequestFavicon(base::BindOnce(
+ [](views::ImageView* favicon_view, const gfx::Image& favicon) {
+ if (!favicon.IsEmpty()) {
+ favicon_view->SetImage(ui::ImageModel::FromImage(favicon));
+ }
+ },
+ favicon_view));
+
+ // Add username/password labels.
+ auto* username_password_view =
+ parent_view->AddChildView(CreateVerticalStackView());
+ username_password_view->SetProperty(views::kBoxLayoutFlexKey,
+ views::BoxLayoutFlexSpecification());
+ username_password_view->AddChildView(
+ CreateUsernameLabel(controller_->GetUsername()));
+ password_label_ = username_password_view->AddChildView(
+ CreatePasswordLabel(controller_->GetNewPassword()));
+
+ // Add eye icon which allows to reveal a password.
+ eye_icon_ = parent_view->AddChildView(views::CreateVectorToggleImageButton(
+ base::BindRepeating(&SuccessfulPasswordChangeView::OnEyeIconClicked,
+ base::Unretained(this))));
+ eye_icon_->SetTooltipText(
+ l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_SHOW_PASSWORD));
+ eye_icon_->SetToggledTooltipText(
+ l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_HIDE_PASSWORD));
+ eye_icon_->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/views/passwords/password_change/successful_password_change_view_unittest.cc b/chrome/browser/ui/views/passwords/password_change/successful_password_change_view_unittest.cc
index 7e29468..d6d78ae 100644
--- a/chrome/browser/ui/views/passwords/password_change/successful_password_change_view_unittest.cc
+++ b/chrome/browser/ui/views/passwords/password_change/successful_password_change_view_unittest.cc
@@ -40,9 +40,11 @@
}
void TearDown() override {
- view_->GetWidget()->CloseWithReason(
- views::Widget::ClosedReason::kUnspecified);
- view_ = nullptr;
+ if (view_) {
+ view_->GetWidget()->CloseWithReason(
+ views::Widget::ClosedReason::kUnspecified);
+ view_ = nullptr;
+ }
PasswordBubbleViewTestBase::TearDown();
}
@@ -55,6 +57,7 @@
}
SuccessfulPasswordChangeView* view() { return view_; }
+ void reset_view() { view_ = nullptr; }
views::Label* GetLabelById(int id) {
return static_cast<views::Label*>(view()->GetViewByID(id));
@@ -128,3 +131,30 @@
EXPECT_TRUE(GetLabelById(SuccessfulPasswordChangeView::kPasswordLabelId)
->GetObscured());
}
+
+TEST_F(SuccessfulPasswordChangeViewTest, AuthCallbackAfterViewDestruction) {
+ CreateAndShowView();
+
+ base::OnceCallback<void(bool)> captured_auth_callback;
+ EXPECT_CALL(*model_delegate_mock(), AuthenticateUserWithMessage)
+ .WillOnce(
+ testing::WithArg<1>([&](base::OnceCallback<void(bool)> callback) {
+ captured_auth_callback = std::move(callback);
+ }));
+
+ views::Button* eye_icon = static_cast<views::Button*>(
+ view()->GetViewByID(SuccessfulPasswordChangeView::kEyeIconButtonId));
+ EXPECT_TRUE(eye_icon);
+
+ views::test::ButtonTestApi(eye_icon).NotifyClick(ui::test::TestEvent());
+ EXPECT_TRUE(captured_auth_callback);
+
+ // Destroy the widget and view before the async auth callback runs.
+ views::Widget* widget = view()->GetWidget();
+ reset_view();
+ widget->CloseNow();
+
+ // Executing the callback when the view is destroyed must not crash or trigger
+ // UAF.
+ std::move(captured_auth_callback).Run(true);
+}
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.
References
On This Page