Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Web Authentication
DescriptionUse after free in Web Authentication
ComponentWeb Authentication
Sub-componentWebAuthn
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Mechanism matches diff. 'Default' preconditions overstated: the test forces an error step via SetCurrentStepForTesting and deletes WebContents from a test observer; the diff does not show a page alone reaching completed-state cancel with synchronous WebContents destruction.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker522566295
Fix commit0ed0be437d70 (chromium/src) +116/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-11 (per vendor advisory)
Fix landed2026-06-11 (public git)
Disclosed2026-06-16
Patch gap5 days — fix public in source before the advisory

Background

`AuthenticatorRequestDialogController`
the browser-side controller that drives the WebAuthn request dialog UI; it owns a model_ pointer and is itself hosted by a WebContents.
`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 WebContents and therefore this controller.
`model_->observers`
the observer list on the controller’s model_, iterated after the step transition to deliver OnCancelRequest() 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.

Key insight
The single mistake is treating 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

  1. Initiate a WebAuthn request A page under attacker control calls navigator.credentials.create({publicKey: ...}) (as in the reproduction test), causing Chrome to construct the AuthenticatorRequestDialogController and its model_.
  2. Drive the request to a completed/error state The request reaches a state where is_request_complete() returns true (the test forces Step::kKeyNotRegistered), so the cancel path will execute the SetCurrentStep(Step::kClosed) branch.
  3. Trigger cancellation CancelAuthenticatorRequest() runs and calls SetCurrentStep(Step::kClosed), which synchronously destroys the hosting WebContents and frees this controller (the test achieves the destruction via an observer that tears down the tab through DetachAndDeleteWebContentsAt on the kClosed transition).
  4. Use-after-free on freed controller Execution continues in the freed method to for (auto& observer : model_->observers), reading model_->observers through the dangling this/model_, which is the UAF.
  5. 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

An attacker who can get a page to start and then cancel a WebAuthn credential request triggers a use-after-free of the 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

FunctionChangeNotes
if
chrome/browser/webauthn/authenticator_request_dialog_controller.cc
modified
for
chrome/browser/webauthn/authenticator_request_dialog_controller.cc
modified
WebAuthnUAFReproductionTest
chrome/browser/webauthn/chrome_webauthn_browsertest.cc
modified
if
chrome/browser/webauthn/chrome_webauthn_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/webauthn/chrome_webauthn_browsertest.cc
modified

Files Changed

  • chrome/browser/webauthn/authenticator_request_dialog_controller.cc
  • chrome/browser/webauthn/chrome_webauthn_browsertest.cc

Audit Directions

  • Self-destroying synchronous callbacks
    Audit any method that calls a step/state transition, observer notification, or UI close (like SetCurrentStep) and then continues to touch this or its members; capture a WeakPtr or reorder so no member access follows a call that can delete the object.
  • `WebContents`-hosted controllers
    Review other objects owned by a WebContents that 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 access
    Look 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.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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:

  1. Register a Chrome Extension that displays a browser action popup containing WebAuthn functionality.
  2. Open the extension popup, and call navigator.credentials.create(...) or get(...) such that an error sheet (like Step::kKeyNotRegistered) is shown in a tab-modal dialog.
  3. The user or script cancels the WebAuthn request (e.g. by dismissing the dialog or aborting with an AbortSignal).
  4. CancelAuthenticatorRequest() runs, calling SetCurrentStep(Step::kClosed), which synchronously destroys the WebAuthn widget.
  5. The widget closure triggers OnWidgetTreeActivated on the ExtensionPopup. It detects that no active web modal is present, causing it to synchronously close itself and delete its hosted WebContents.
  6. The WebContents deletion destroys AuthenticatorImpl, which deallocates the AuthenticatorRequestDialogController (this).
  7. 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.

View on issue tracker