CVE-2026-12443
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/webauthn/authenticator_request_dialog_controller.cc |
modified | |
forchrome/browser/webauthn/authenticator_request_dialog_controller.cc |
modified | |
WebAuthnUAFReproductionTestchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified | |
ifchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/webauthn/chrome_webauthn_browsertest.cc |
modified |
Files Changed
chrome/browser/webauthn/authenticator_request_dialog_controller.ccchrome/browser/webauthn/chrome_webauthn_browsertest.cc
Patch
From 0ed0be437d706f293a8249ede74f177476abce68 Mon Sep 17 00:00:00 2001
From: Adem Derinel <derinel@google.com>
Date: Thu, 11 Jun 2026 07:20:53 -0700
Subject: [PATCH] WebAuthn: Fix UAF in CancelAuthenticatorRequest
When is_request_complete() is true, CancelAuthenticatorRequest calls
SetCurrentStep(Step::kClosed) which can synchronously destroy the hosting
WebContents and therefore the controller itself. Subsequent access to
model_->observers results in a Use-After-Free.
This CL adds a WeakPtr guard to return early if the controller is destroyed.
TAG=agy
Fixed: 522566295
Change-Id: I0aff1f3d71bd790cb73ec2967bca7bd1ca531879
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7921992
Commit-Queue: Adem Derinel <derinel@google.com>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1645345}
---
diff --git a/chrome/browser/webauthn/authenticator_request_dialog_controller.cc b/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
index 7ee13e1..875d03f 100644
--- a/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
+++ b/chrome/browser/webauthn/authenticator_request_dialog_controller.cc
@@ -624,9 +624,16 @@
return;
}
+ // SetCurrentStep(Step::kClosed) can synchronously destroy the hosting
+ // WebContents and therefore `this`. See crbug.com/522566295.
+ base::WeakPtr<AuthenticatorRequestDialogController> weak_this =
+ weak_factory_.GetWeakPtr();
if (is_request_complete()) {
SetCurrentStep(Step::kClosed);
}
+ if (!weak_this) {
+ return;
+ }
for (auto& observer : model_->observers) {
observer.OnCancelRequest();
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index 5e912ff..e6227ba 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -1864,4 +1864,113 @@
testing::HasSubstr("error SecurityError"));
}
+// Reproduction test for Use-After-Free in AuthenticatorRequestDialogController
+// (crbug.com/522566295).
+class WebAuthnUAFReproductionTest
+ : public WebAuthnBrowserTest,
+ public ChromeAuthenticatorRequestDelegate::TestObserver,
+ public AuthenticatorRequestDialogModel::Observer {
+ public:
+ WebAuthnUAFReproductionTest() = default;
+ ~WebAuthnUAFReproductionTest() override = default;
+
+ // ChromeAuthenticatorRequestDelegate::TestObserver:
+ void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
+ delegate_ = delegate;
+ model_ = delegate_->dialog_model();
+ model_->AddObserver(this);
+ }
+
+ void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
+ if (run_loop_) {
+ run_loop_->Quit();
+ }
+ }
+
+ void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
+ delegate_ = nullptr;
+ }
+
+ // AuthenticatorRequestDialogModel::Observer:
+ void OnStepTransition() override {
+ if (model_ &&
+ model_->step() == AuthenticatorRequestDialogModel::Step::kClosed) {
+ DeleteWebContents();
+ }
+ }
+
+ void OnModelDestroyed(AuthenticatorRequestDialogModel* model) override {
+ if (model_ == model) {
+ model_ = nullptr;
+ }
+ }
+
+ void DeleteWebContents() {
+ if (web_contents_deleted_) {
+ return;
+ }
+ web_contents_deleted_ = true;
+ auto* tab_strip_model = browser()->tab_strip_model();
+ int active_index = tab_strip_model->active_index();
+ tab_strip_model->DetachAndDeleteWebContentsAt(active_index);
+ }
+
+ void SetUpOnMainThread() override {
+ WebAuthnBrowserTest::SetUpOnMainThread();
+ ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(this);
+ }
+
+ void TearDownOnMainThread() override {
+ if (model_) {
+ model_->RemoveObserver(this);
+ }
+ ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
+ WebAuthnBrowserTest::TearDownOnMainThread();
+ }
+
+ raw_ptr<ChromeAuthenticatorRequestDelegate> delegate_ = nullptr;
+ raw_ptr<AuthenticatorRequestDialogModel> model_ = nullptr;
+ std::unique_ptr<base::RunLoop> run_loop_;
+ bool web_contents_deleted_ = false;
+};
+
+IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, CancelUAF) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), https_server_.GetURL("www.example.com", "/title1.html")));
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Trigger WebAuthn flow asynchronously
+ content::ExecuteScriptAsync(web_contents, R"(
+ navigator.credentials.create({
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ rp: { name: "Example" },
+ user: {
+ id: new Uint8Array([1, 2, 3, 4]),
+ name: "test",
+ displayName: "test"
+ },
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+ timeout: 60000
+ }
+ });
+ )");
+
+ if (!delegate_) {
+ run_loop_ = std::make_unique<base::RunLoop>();
+ run_loop_->Run();
+ }
+ ASSERT_TRUE(delegate_);
+ ASSERT_TRUE(delegate_->dialog_controller());
+
+ // Force controller step to an error state where is_request_complete() is true
+ delegate_->dialog_controller()->SetCurrentStepForTesting(
+ AuthenticatorRequestDialogModel::Step::kKeyNotRegistered);
+
+ // Trigger UAF
+ delegate_->dialog_controller()->CancelAuthenticatorRequest();
+}
+
} // namespace
Regression Test / PoC
diff --git a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
index 5e912ff..e6227ba 100644
--- a/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
+++ b/chrome/browser/webauthn/chrome_webauthn_browsertest.cc
@@ -1864,4 +1864,113 @@
testing::HasSubstr("error SecurityError"));
}
+// Reproduction test for Use-After-Free in AuthenticatorRequestDialogController
+// (crbug.com/522566295).
+class WebAuthnUAFReproductionTest
+ : public WebAuthnBrowserTest,
+ public ChromeAuthenticatorRequestDelegate::TestObserver,
+ public AuthenticatorRequestDialogModel::Observer {
+ public:
+ WebAuthnUAFReproductionTest() = default;
+ ~WebAuthnUAFReproductionTest() override = default;
+
+ // ChromeAuthenticatorRequestDelegate::TestObserver:
+ void Created(ChromeAuthenticatorRequestDelegate* delegate) override {
+ delegate_ = delegate;
+ model_ = delegate_->dialog_model();
+ model_->AddObserver(this);
+ }
+
+ void UIShown(ChromeAuthenticatorRequestDelegate* delegate) override {
+ if (run_loop_) {
+ run_loop_->Quit();
+ }
+ }
+
+ void OnDestroy(ChromeAuthenticatorRequestDelegate* delegate) override {
+ delegate_ = nullptr;
+ }
+
+ // AuthenticatorRequestDialogModel::Observer:
+ void OnStepTransition() override {
+ if (model_ &&
+ model_->step() == AuthenticatorRequestDialogModel::Step::kClosed) {
+ DeleteWebContents();
+ }
+ }
+
+ void OnModelDestroyed(AuthenticatorRequestDialogModel* model) override {
+ if (model_ == model) {
+ model_ = nullptr;
+ }
+ }
+
+ void DeleteWebContents() {
+ if (web_contents_deleted_) {
+ return;
+ }
+ web_contents_deleted_ = true;
+ auto* tab_strip_model = browser()->tab_strip_model();
+ int active_index = tab_strip_model->active_index();
+ tab_strip_model->DetachAndDeleteWebContentsAt(active_index);
+ }
+
+ void SetUpOnMainThread() override {
+ WebAuthnBrowserTest::SetUpOnMainThread();
+ ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(this);
+ }
+
+ void TearDownOnMainThread() override {
+ if (model_) {
+ model_->RemoveObserver(this);
+ }
+ ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting(nullptr);
+ WebAuthnBrowserTest::TearDownOnMainThread();
+ }
+
+ raw_ptr<ChromeAuthenticatorRequestDelegate> delegate_ = nullptr;
+ raw_ptr<AuthenticatorRequestDialogModel> model_ = nullptr;
+ std::unique_ptr<base::RunLoop> run_loop_;
+ bool web_contents_deleted_ = false;
+};
+
+IN_PROC_BROWSER_TEST_F(WebAuthnUAFReproductionTest, CancelUAF) {
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(
+ browser(), https_server_.GetURL("www.example.com", "/title1.html")));
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Trigger WebAuthn flow asynchronously
+ content::ExecuteScriptAsync(web_contents, R"(
+ navigator.credentials.create({
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ rp: { name: "Example" },
+ user: {
+ id: new Uint8Array([1, 2, 3, 4]),
+ name: "test",
+ displayName: "test"
+ },
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+ timeout: 60000
+ }
+ });
+ )");
+
+ if (!delegate_) {
+ run_loop_ = std::make_unique<base::RunLoop>();
+ run_loop_->Run();
+ }
+ ASSERT_TRUE(delegate_);
+ ASSERT_TRUE(delegate_->dialog_controller());
+
+ // Force controller step to an error state where is_request_complete() is true
+ delegate_->dialog_controller()->SetCurrentStepForTesting(
+ AuthenticatorRequestDialogModel::Step::kKeyNotRegistered);
+
+ // Trigger UAF
+ delegate_->dialog_controller()->CancelAuthenticatorRequest();
+}
+
} // namespace
Original Bug Report
Potential Browser Use-After-Free in AuthenticatorRequestDialogController::CancelAuthenticatorRequest
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 potential Use-After-Free (UAF) vulnerability exists in the WebAuthn UI controller’s cancel request handler. Changing the dialog step to closed can synchronously trigger the teardown of the hosting WebContents, destroying the controller while it is still executing. Subsequent instructions on the call stack access and dereference a member pointer on the freed controller.
Affected files:
chrome/browser/webauthn/authenticator_request_dialog_controller.cc
Estimated timestamp from git blame: 2019-02-06
Root Cause Analysis
In chrome/browser/webauthn/authenticator_request_dialog_controller.cc, the function CancelAuthenticatorRequest() contains the following logic:
void AuthenticatorRequestDialogController::CancelAuthenticatorRequest() {
...
if (is_request_complete()) {
SetCurrentStep(Step::kClosed); // [1] May synchronously free `this`
}
for (auto& observer : model_->observers) { // [2] Use-After-Free read of `this->model_`
observer.OnCancelRequest();
}
}
When is_request_complete() is true (such as when the UI is on the Step::kKeyNotRegistered or Step::kTimedOut error sheets), calling SetCurrentStep(Step::kClosed) updates the model step to Step::kClosed (chrome/browser/webauthn/authenticator_request_dialog_model.cc:157). This computes the new step UI type as StepUIType::NONE, causing the model to call view_controller_.reset() and synchronously destroy the underlying constrained tab-modal views::Widget.
Under certain conditions—most notably when the WebAuthn flow is hosted within an Extension Popup bubble (ExtensionPopup in chrome/browser/ui/views/extensions/extension_popup.cc)—destroying the modal widget changes the focus and triggers OnWidgetTreeActivated inside ExtensionPopup. Since the web-modal dialog has closed, the popup immediately schedules self-destruction via CloseDeferredIfNecessary(), which synchronously deletes the ExtensionViewHost and its owned WebContents.
This synchronous teardown propagates down through ~AuthenticatorImpl (Mojo DocumentService) -> ~AuthenticatorCommonImpl -> ~RequestState -> ~ChromeAuthenticatorRequestDelegate -> ~AuthenticatorRequestDialogController. This frees the controller (this) allocation while it is still executing. When SetCurrentStep returns, the call stack dereferences the freed controller to read model_->observers, resulting in a Use-After-Free (UAF).
Potential Attack Vector / Reproduction Steps
As our tooling agent does not have the capability to run code or execute a proof of concept, these are suggested/potential steps that should lead to triggering the vulnerability:
- Register a Chrome Extension that displays a browser action popup containing WebAuthn functionality.
- Open the extension popup, and call
navigator.credentials.create(...)orget(...)such that an error sheet (likeStep::kKeyNotRegistered) is shown in a tab-modal dialog. - The user or script cancels the WebAuthn request (e.g. by dismissing the dialog or aborting with an
AbortSignal). CancelAuthenticatorRequest()runs, callingSetCurrentStep(Step::kClosed), which synchronously destroys the WebAuthn widget.- The widget closure triggers
OnWidgetTreeActivatedon theExtensionPopup. It detects that no active web modal is present, causing it to synchronously close itself and delete its hostedWebContents. - The
WebContentsdeletion destroysAuthenticatorImpl, which deallocates theAuthenticatorRequestDialogController(this). - The call stack returns to the controller and accesses
this->model_on the freed memory, causing a UAF read and potential indirect virtual call hijack.
Suggested Fix
To prevent the UAF, use the existing base::WeakPtr of the controller to check whether this has been synchronously destroyed during the step transition before accessing any member variables:
void AuthenticatorRequestDialogController::CancelAuthenticatorRequest() {
...
base::WeakPtr<AuthenticatorRequestDialogController> weak_this = GetWeakPtr();
if (is_request_complete()) {
SetCurrentStep(Step::kClosed);
}
if (!weak_this) {
return;
}
for (auto& observer : model_->observers) {
observer.OnCancelRequest();
}
}
Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141
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.