Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Autofill
DescriptionUse after free in Autofill
ComponentAutofill
Bug ClassUAF
Tracker505191883
Fix commit60a432d27fbf (chromium/src) +28/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm
modified

Files Changed

  • ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm
From 60a432d27fbf9b047b99035179d7d4d767a53f06 Mon Sep 17 00:00:00 2001
From: Jean-Francois Le <jfle@google.com>
Date: Mon, 27 Apr 2026 06:19:28 -0700
Subject: [PATCH] [iOS] Use WeakPtr for webStateList in manual filling

Fixed: 505191883
Change-Id: Ic9747a0d340b1d354a320dde97c4da341947039f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7786010
Reviewed-by: Vincent Boisselle <vincb@google.com>
Reviewed-by: Tommy Martino <tmartino@chromium.org>
Commit-Queue: Jean-François Le <jfle@google.com>
Cr-Commit-Position: refs/heads/main@{#1621040}
---

diff --git a/ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm b/ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm
index d3b73938..bc930d2 100644
--- a/ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm
+++ b/ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm
@@ -77,9 +77,6 @@
 // hardware for authentication is available.
 @property(nonatomic, strong) ReauthenticationModule* reauthenticationModule;
 
-// The WebStateList with the relevant active web state for the injection.
-@property(nonatomic, assign) WebStateList* webStateList;
-
 // YES if the last focused element is secure within its web frame. To be secure
 // means the web is HTTPS and the URL is trusted.
 @property(nonatomic, assign, getter=isLastFocusedElementSecure)
@@ -109,6 +106,9 @@
   // given `webState`. This is there to solve a dependency cycle between model/
   // and ui_bundled/.
   AutofillProviderGetter _autofillProviderGetter;
+
+  // The WebStateList with the relevant active web state for the injection.
+  base::WeakPtr<WebStateList> _webStateList;
 }
 
 - (instancetype)
@@ -119,7 +119,7 @@
     autofillProviderGetter:(AutofillProviderGetter)autofillProviderGetter {
   self = [super init];
   if (self) {
-    _webStateList = webStateList;
+    _webStateList = webStateList->AsWeakPtr();
     _securityAlertHandler = securityAlertHandler;
     _formHelper =
         [[FormObserverHelper alloc] initWithWebStateList:webStateList];
@@ -195,9 +195,10 @@
                       shouldReauth:(BOOL)shouldReauth {
   if (shouldReauth && [self.reauthenticationModule canAttemptReauth]) {
     NSString* reason = l10n_util::GetNSString(IDS_IOS_AUTOFILL_REAUTH_REASON);
+    __weak __typeof(self) weakSelf = self;
     auto completionHandler = ^(ReauthenticationResult result) {
       if (result != ReauthenticationResult::kFailure) {
-        [self fillFormWithCredential:credential];
+        [weakSelf fillFormWithCredential:credential];
       }
     };
 
@@ -236,7 +237,10 @@
 }
 
 - (BOOL)isActiveFormAPasswordForm {
-  web::WebState* activeWebState = self.webStateList->GetActiveWebState();
+  if (!_webStateList) {
+    return NO;
+  }
+  web::WebState* activeWebState = _webStateList->GetActiveWebState();
   if (!activeWebState) {
     return NO;
   }
@@ -285,7 +289,10 @@
 
 // Injects the passed string to the active field and jumps to the next field.
 - (void)fillLastSelectedFieldWithString:(NSString*)string {
-  web::WebState* activeWebState = self.webStateList->GetActiveWebState();
+  if (!_webStateList) {
+    return;
+  }
+  web::WebState* activeWebState = _webStateList->GetActiveWebState();
   if (!activeWebState) {
     return;
   }
@@ -300,9 +307,10 @@
   data.Set("renderer_id",
            static_cast<int>([self lastFocusedElementUniqueID].value()));
   data.Set("value", base::SysNSStringToUTF16(string));
+  __weak __typeof(self) weakSelf = self;
   autofill::AutofillJavaScriptFeature::GetInstance()->FillActiveFormField(
       activeWebFrame, std::move(data), base::BindOnce(^(BOOL success) {
-        [self jumpToNextField];
+        [weakSelf jumpToNextField];
       }));
 }
 
@@ -310,7 +318,10 @@
 - (void)jumpToNextField {
   FormInputAccessoryViewHandler* handler =
       [[FormInputAccessoryViewHandler alloc] init];
-  handler.webState = self.webStateList->GetActiveWebState();
+  if (!_webStateList) {
+    return;
+  }
+  handler.webState = _webStateList->GetActiveWebState();
   [handler setLastFocusFormActivityWebFrameID:
                base::SysUTF8ToNSString(self.lastFocusedElementFrameIdentifier)];
   [handler selectNextElementWithoutButtonPress];
@@ -319,7 +330,10 @@
 // Fills the current form with the given `credential`. Only works if the current
 // form is a password form, otherwise it's a no-op.
 - (void)fillFormWithCredential:(ManualFillCredential*)credential {
-  web::WebState* activeWebState = self.webStateList->GetActiveWebState();
+  if (!_webStateList) {
+    return;
+  }
+  web::WebState* activeWebState = _webStateList->GetActiveWebState();
   if (!activeWebState) {
     return;
   }
@@ -451,7 +465,10 @@
 - (id<FormSuggestionProvider>)providerForSuggestion:
     (FormSuggestion*)suggestion {
   if (IsSupportedSuggestion(suggestion)) {
-    return _autofillProviderGetter.Run(self.webStateList->GetActiveWebState());
+    if (!_webStateList) {
+      return nil;
+    }
+    return _autofillProviderGetter.Run(_webStateList->GetActiveWebState());
   }
 
   // The manual fill injector should not use Suggestion objects for any other
Loading diff…

Original Bug Report

reported by li...@chromium.org

Potential Use-After-Free in ManualFillInjectionHandler during asynchronous Face ID re-authentication

Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A Use-After-Free vulnerability exists in ManualFillInjectionHandler on iOS when Face ID is required for autofill. The handler strongly captures self in the re-authentication block while holding an unsafe raw pointer (assign) to a WebStateList. If the last Incognito tab is closed by a script while the Face ID prompt is pending, the WebStateList is destroyed, leading to a UAF when the callback executes.

Affected files:

  • ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm

Estimated timestamp from git blame: 2024-07-24

Summary

A Use-After-Free (UAF) vulnerability exists in ManualFillInjectionHandler on iOS. The handler holds a raw pointer to a WebStateList and captures self strongly in the completion block for Face ID / Touch ID re-authentication. If the WebStateList is destroyed (e.g., when the last Incognito tab is closed) while the system authentication prompt is pending, the handler remains alive via the strong reference and dereferences the dangling WebStateList pointer when the callback eventually executes.

Technical Details

In ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_injection_handler.mm, the ManualFillInjectionHandler class holds a raw pointer to WebStateList via an Objective-C assign property:

@property(nonatomic, assign) WebStateList* webStateList;

Because WebStateList is a C++ object, assign acts as a raw pointer without ARC tracking or zeroing behavior.

In autofillFormWithCredential:shouldReauth:, the completion block for re-authentication captures self strongly:

auto completionHandler = ^(ReauthenticationResult result) {
  if (result != ReauthenticationResult::kFailure) {
    [self fillFormWithCredential:credential];
  }
};

When attemptReauthWithLocalizedReason:... is called, the system Face ID prompt is presented. This prompt is out-of-process and does not pause JavaScript execution in the background WKWebView.

If the active tab is closed via JavaScript (window.close()) while the prompt is visible, and it is the last Incognito tab, the OffTheRecord Profile is destroyed. This destroys the Browser and its associated WebStateList. However, the pending Face ID block keeps the ManualFillInjectionHandler alive.

Once the user successfully authenticates, the block executes and calls fillFormWithCredential:, which immediately attempts to access the destroyed WebStateList:

web::WebState* activeWebState = self.webStateList->GetActiveWebState();

This results in a Use-After-Free in the browser process.

Potential Exploitation Scenario

Note: These are potential steps as our tooling cannot currently run live code to provide a functional PoC.

  1. An attacker tricks a user into opening an Incognito tab (e.g. A.html).
  2. The user clicks a link in A.html that triggers window.open('B.html'), opening a new Incognito tab. Because it was opened via script, WebKit allows B.html to later call window.close() on itself.
  3. The attacker socially engineers the user into manually closing A.html (e.g., “Please close other tabs for security”), leaving B.html as the sole Incognito tab.
  4. B.html presents a fake login form.
  5. The user taps the form, triggering Chrome’s manual autofill accessory.
  6. The page’s JavaScript detects the focus and starts a timer: setTimeout(function() { window.close(); }, 5000);.
  7. The user selects a credential, triggering the Face ID prompt.
  8. While the prompt is active, the timer fires and B.html closes itself. Since it was the last Incognito tab, the WebStateList is destroyed.
  9. The user completes Face ID. The callback executes, dereferences the dangling WebStateList, and triggers the UAF, potentially leading to Remote Code Execution / Sandbox Escape in the browser process if the heap was groomed.
  1. Use Weak Capture: The completionHandler in autofillFormWithCredential:shouldReauth: and the block in fillLastSelectedFieldWithString: should capture self weakly (__weak __typeof(self) weakSelf = self;) to prevent the handler from outliving its owner.
  2. Safe Pointers: Change the webStateList property from assign to a safer construct. If using a raw pointer is necessary, the class should observe the destruction of the WebStateList (or its owning Browser) and clear the pointer before it becomes dangling.

Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a


Results 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.

View on issue tracker