CVE-2026-10951
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/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
Patch
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
Original Bug Report
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.
- An attacker tricks a user into opening an Incognito tab (e.g.
A.html). - The user clicks a link in
A.htmlthat triggerswindow.open('B.html'), opening a new Incognito tab. Because it was opened via script, WebKit allowsB.htmlto later callwindow.close()on itself. - The attacker socially engineers the user into manually closing
A.html(e.g., “Please close other tabs for security”), leavingB.htmlas the sole Incognito tab. B.htmlpresents a fake login form.- The user taps the form, triggering Chrome’s manual autofill accessory.
- The page’s JavaScript detects the focus and starts a timer:
setTimeout(function() { window.close(); }, 5000);. - The user selects a credential, triggering the Face ID prompt.
- While the prompt is active, the timer fires and
B.htmlcloses itself. Since it was the last Incognito tab, theWebStateListis destroyed. - 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.
Recommended Fix
- Use Weak Capture: The
completionHandlerinautofillFormWithCredential:shouldReauth:and the block infillLastSelectedFieldWithString:should captureselfweakly (__weak __typeof(self) weakSelf = self;) to prevent the handler from outliving its owner. - Safe Pointers: Change the
webStateListproperty fromassignto a safer construct. If using a raw pointer is necessary, the class should observe the destruction of theWebStateList(or its owningBrowser) 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.