CVE-2026-12443
Overview
Background
- `AuthenticatorRequestDialogController`
- the browser-side controller that drives the WebAuthn request dialog UI; it owns a
model_pointer and is itself hosted by aWebContents. - `CancelAuthenticatorRequest`
- the controller method (named in the commit subject and invoked by the reproduction test) that tears down an in-flight WebAuthn request and notifies observers.
- `SetCurrentStep(Step::kClosed)`
- a state transition that closes the dialog; per the patch comment it can synchronously destroy the hosting
WebContentsand thereforethiscontroller. - `model_->observers`
- the observer list on the controller’s
model_, iterated after the step transition to deliverOnCancelRequest()callbacks.
Root Cause Analysis
In CancelAuthenticatorRequest, when is_request_complete() is true the code calls SetCurrentStep(Step::kClosed) and then, unconditionally, iterates model_->observers to invoke observer.OnCancelRequest(). As the added comment states, SetCurrentStep(Step::kClosed) “can synchronously destroy the hosting WebContents and therefore this”, which frees the controller (and its model_ member) before control returns. The subsequent model_->observers access therefore dereferences freed memory — a use-after-free — because the method assumed the controller would outlive the step transition, an invariant that closing the dialog violates.
The fix captures base::WeakPtr<AuthenticatorRequestDialogController> weak_this = weak_factory_.GetWeakPtr() before the transition and, after SetCurrentStep, checks if (!weak_this) { return; }, bailing out when this has been destroyed and thus never reaching the freed model_->observers. Because the guard runs before the observer loop, the loop only executes when the controller is still alive.
SetCurrentStep(Step::kClosed) as a benign state change when it can synchronously delete the enclosing WebContents and the controller itself, leaving the following model_->observers iteration operating on freed memory; the fix detects that self-destruction with a WeakPtr guard and returns early.Attack Path
- Initiate a WebAuthn request
A page under attacker control calls
navigator.credentials.create({publicKey: ...})(as in the reproduction test), causing Chrome to construct theAuthenticatorRequestDialogControllerand itsmodel_. - Drive the request to a completed/error state
The request reaches a state where
is_request_complete()returns true (the test forcesStep::kKeyNotRegistered), so the cancel path will execute theSetCurrentStep(Step::kClosed)branch. - Trigger cancellation
CancelAuthenticatorRequest()runs and callsSetCurrentStep(Step::kClosed), which synchronously destroys the hostingWebContentsand freesthiscontroller (the test achieves the destruction via an observer that tears down the tab throughDetachAndDeleteWebContentsAton thekClosedtransition). - Use-after-free on freed controller
Execution continues in the freed method to
for (auto& observer : model_->observers), readingmodel_->observersthrough the danglingthis/model_, which is the UAF. - Exploitation of the freed allocation
Controlling the contents of the reclaimed memory to turn the dangling read/observer dispatch into a stronger primitive or code execution is
(inferred); the diff proves only the use-after-free, not any specific downstream primitive.
Impact Assessment
AuthenticatorRequestDialogController in the browser process, the privileged process that hosts this UI. The advisory classifies this as a use-after-free in Web Authentication but does not report exploited-in-the-wild status; any escalation from the freed-object access to memory corruption, a read/write primitive, or code execution in the browser process is (inferred) and not demonstrated by the patch. The precondition is a request state where is_request_complete() is true at the moment CancelAuthenticatorRequest runs so the SetCurrentStep(Step::kClosed) self-destruction path executes.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
Audit Directions
- Self-destroying synchronous callbacksAudit any method that calls a step/state transition, observer notification, or UI close (like
SetCurrentStep) and then continues to touchthisor its members; capture aWeakPtror reorder so no member access follows a call that can delete the object. - `WebContents`-hosted controllersReview other objects owned by a
WebContentsthat dispatch to observer lists after operations that may tear down the tab, since those are the same free-then-use shape fixed here. - Post-notification member accessLook for iterations over
model_->observers(or similar owned collections) that run after a transition, where reentrant destruction during the transition can invalidate the collection before the loop.
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.