CVE-2026-17724
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/web/web_state/ui/crw_web_controller.mm |
modified | |
ExecuteJavaScriptios/web/web_state/ui/crw_web_controller.mm |
modified | |
BindOnceios/web/web_state/ui/crw_web_controller.mm |
modified | |
TEST_Fios/web/web_state/ui/crw_web_controller_unittest.mm |
modified |
Files Changed
ios/web/web_state/ui/crw_web_controller.mmios/web/web_state/ui/crw_web_controller_unittest.mm
Patch
From 35270a4f5b614cf01f60bbdbe1553438b5a6be3b Mon Sep 17 00:00:00 2001
From: Mike Dougherty <michaeldo@chromium.org>
Date: Thu, 25 Jun 2026 13:21:29 -0700
Subject: [PATCH] Bind executeUserJavaScript calls to current WebFrame
Fixed: 523720739
Change-Id: Ieda759f70c455427d705030369d0f7eed63da8d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7988133
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1652666}
---
diff --git a/ios/web/web_state/ui/crw_web_controller.mm b/ios/web/web_state/ui/crw_web_controller.mm
index 8a01791..80b60a6 100644
--- a/ios/web/web_state/ui/crw_web_controller.mm
+++ b/ios/web/web_state/ui/crw_web_controller.mm
@@ -33,6 +33,7 @@
#import "ios/web/history_state_util.h"
#import "ios/web/js_features/scroll_helper/scroll_helper_java_script_feature.h"
#import "ios/web/js_messaging/java_script_feature_util_impl.h"
+#import "ios/web/js_messaging/web_frame_impl.h"
#import "ios/web/js_messaging/web_view_js_utils.h"
#import "ios/web/js_messaging/web_view_web_state_map.h"
#import "ios/web/navigation/back_forward_navigation_type.h"
@@ -49,6 +50,7 @@
#import "ios/web/public/annotations/annotations_text_manager.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/find_in_page/crw_find_interaction.h"
+#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/permissions/permissions.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_client.h"
@@ -152,6 +154,31 @@
return YES;
}
+typedef void (^JavaScriptCompletionBlock)(id, NSError*);
+
+// Wraps the completion block to log script execution failures as warnings and,
+// if enabled, assert on JavaScript errors.
+JavaScriptCompletionBlock WrapCompletionBlock(
+ JavaScriptCompletionBlock completion) {
+ __block JavaScriptCompletionBlock stack_completion = [completion copy];
+ return ^(id value, NSError* error) {
+ if (error) {
+ DLOG(WARNING) << "Script execution failed with error: "
+ << base::SysNSStringToUTF16(
+ error.userInfo[NSLocalizedDescriptionKey]);
+
+ if (base::FeatureList::IsEnabled(
+ web::features::kAssertOnJavaScriptErrors)) {
+ CHECK(false) << "JavaScript error occurred with "
+ "kAssertOnJavaScriptErrors enabled.";
+ }
+ }
+ if (stack_completion) {
+ stack_completion(value, error);
+ }
+ };
+}
+
} // namespace
@interface CRWWebController () <CRWDataControlsDelegate,
@@ -1089,32 +1116,18 @@
- (void)executeJavaScript:(NSString*)javascript
completionHandler:(void (^)(id result, NSError* error))completion {
- __block void (^stack_completion_block)(id result, NSError* error) =
- [completion copy];
- web::ExecuteJavaScript(self.webView, javascript, ^(id value, NSError* error) {
- if (error) {
- DLOG(WARNING) << "Script execution failed with error: "
- << base::SysNSStringToUTF16(
- error.userInfo[NSLocalizedDescriptionKey]);
-
- if (base::FeatureList::IsEnabled(
- web::features::kAssertOnJavaScriptErrors)) {
- CHECK(false) << "JavaScript error occurred with "
- "kAssertOnJavaScriptErrors enabled.";
- }
- }
- if (stack_completion_block) {
- stack_completion_block(value, error);
- }
- });
+ web::ExecuteJavaScript(self.webView, javascript,
+ WrapCompletionBlock(completion));
}
- (void)executeUserJavaScript:(NSString*)javascript
completionHandler:(void (^)(id result, NSError* error))completion {
- // For security reasons, executing JavaScript on pages with app-specific URLs
- // is not allowed, because those pages may have elevated privileges.
- if (web::GetWebClient()->IsAppSpecificURL(
- self.webStateImpl->GetLastCommittedURL())) {
+ web::WebFrame* mainFrame =
+ self.webStateImpl->GetPageWorldWebFramesManager()->GetMainWebFrame();
+ if (!mainFrame ||
+ // For security reasons, executing JavaScript on pages with app-specific
+ // URLs is not allowed, because those pages may have elevated privileges.
+ web::GetWebClient()->IsAppSpecificURL(mainFrame->GetUrl())) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
NSError* error = [[NSError alloc]
@@ -1129,7 +1142,12 @@
[self touched:YES];
- [self executeJavaScript:javascript completionHandler:completion];
+ mainFrame->ExecuteJavaScript(
+ base::SysNSStringToUTF16(javascript),
+ base::BindOnce(^(const base::Value* value, NSError* error) {
+ id foundation_result = web::NSObjectFromValueResult(value);
+ WrapCompletionBlock(completion)(foundation_result, error);
+ }));
}
#pragma mark - CRWTouchTrackingDelegate (Public)
diff --git a/ios/web/web_state/ui/crw_web_controller_unittest.mm b/ios/web/web_state/ui/crw_web_controller_unittest.mm
index 25dd689e2..0cc4a777 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -1462,17 +1462,25 @@
// URLs have elevated privileges and JavaScript execution should not be allowed
// for them.
TEST_F(ScriptExecutionTest, UserScriptOnAppSpecificPage) {
+ LoadHtml(@"<html></html>", GURL(kTestAppSpecificURL));
+
+ NSError* error = nil;
+ EXPECT_FALSE(ExecuteUserJavaScript(@"window.w = 0;", &error));
+ ASSERT_TRUE(error);
+ EXPECT_NSEQ(kJSEvaluationErrorDomain, error.domain);
+ EXPECT_EQ(JS_EVALUATION_ERROR_CODE_REJECTED, error.code);
+
+ EXPECT_FALSE(ExecuteJavaScript(@"window.w"));
+}
+
+// Tests that user script is rejected when there is no main frame to execute it
+// in.
+TEST_F(ScriptExecutionTest, UserScriptRejectedWithoutMainFrame) {
LoadHtml(@"<html></html>", GURL(kTestURLString));
- // Change last committed URL to app-specific URL.
- NavigationManagerImpl& nav_manager =
- [web_controller() webStateImpl]->GetNavigationManagerImpl();
- nav_manager.AddPendingItem(
- GURL(kTestAppSpecificURL), Referrer(), ui::PAGE_TRANSITION_TYPED,
- NavigationInitiationType::BROWSER_INITIATED,
- /*is_post_navigation=*/false, /*is_error_navigation=*/false,
- web::HttpsUpgradeType::kNone);
- nav_manager.CommitPendingItem();
+ // Simulate the embedder having no main frame for the current page (e.g.
+ // because the page navigated away before frame registration completed).
+ [web_controller() webStateImpl]->RemoveAllWebFrames();
NSError* error = nil;
EXPECT_FALSE(ExecuteUserJavaScript(@"window.w = 0;", &error));
Regression Test / PoC
diff --git a/ios/web/web_state/ui/crw_web_controller_unittest.mm b/ios/web/web_state/ui/crw_web_controller_unittest.mm
index 25dd689e2..0cc4a777 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -1462,17 +1462,25 @@
// URLs have elevated privileges and JavaScript execution should not be allowed
// for them.
TEST_F(ScriptExecutionTest, UserScriptOnAppSpecificPage) {
+ LoadHtml(@"<html></html>", GURL(kTestAppSpecificURL));
+
+ NSError* error = nil;
+ EXPECT_FALSE(ExecuteUserJavaScript(@"window.w = 0;", &error));
+ ASSERT_TRUE(error);
+ EXPECT_NSEQ(kJSEvaluationErrorDomain, error.domain);
+ EXPECT_EQ(JS_EVALUATION_ERROR_CODE_REJECTED, error.code);
+
+ EXPECT_FALSE(ExecuteJavaScript(@"window.w"));
+}
+
+// Tests that user script is rejected when there is no main frame to execute it
+// in.
+TEST_F(ScriptExecutionTest, UserScriptRejectedWithoutMainFrame) {
LoadHtml(@"<html></html>", GURL(kTestURLString));
- // Change last committed URL to app-specific URL.
- NavigationManagerImpl& nav_manager =
- [web_controller() webStateImpl]->GetNavigationManagerImpl();
- nav_manager.AddPendingItem(
- GURL(kTestAppSpecificURL), Referrer(), ui::PAGE_TRANSITION_TYPED,
- NavigationInitiationType::BROWSER_INITIATED,
- /*is_post_navigation=*/false, /*is_error_navigation=*/false,
- web::HttpsUpgradeType::kNone);
- nav_manager.CommitPendingItem();
+ // Simulate the embedder having no main frame for the current page (e.g.
+ // because the page navigated away before frame registration completed).
+ [web_controller() webStateImpl]->RemoveAllWebFrames();
NSError* error = nil;
EXPECT_FALSE(ExecuteUserJavaScript(@"window.w = 0;", &error));
Original Bug Report
Potential TOCTOU UXSS in LoadJavaScriptURL via Unbound evaluateJavaScript API
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 Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in iOS Chrome’s handling of bookmarklet execution. By initiating a cross-origin navigation while flooding the IPC channel, an attacker could bypass UI-side URL security checks. This could result in malicious JavaScript being evaluated on a newly navigated sensitive origin, leading to Universal Cross-Site Scripting (UXSS).
Affected files:
ios/web/js_messaging/web_view_js_utils.mmios/chrome/browser/url_loading/model/url_loading_util.mmios/web/web_state/ui/crw_web_controller.mmios/web/navigation/navigation_manager_impl.mm
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential Time-of-Check to Time-of-Use (TOCTOU) race condition exists in the implementation of LoadJavaScriptURL on iOS. When a user triggers a bookmarklet, Chrome performs URL verification in the UI process to ensure the script executes on the intended page. For example, BookmarksCoordinator checks that activeWebState->GetLastCommittedURL() matches the expected URL before dismissal.
However, the actual execution ultimately relies on the legacy [WKWebView evaluateJavaScript:completionHandler:] API in ios/web/js_messaging/web_view_js_utils.mm. This specific API is not frame-bound. If a navigation commits in the WebContent process after Chrome dispatches the execution request but before WebKit evaluates it, the script will execute in the context of the newly committed page.
An attacker can widen this race window significantly by flooding the UI process with IPC messages (e.g., via window.webkit.messageHandlers). This delays the UI process from receiving navigation commit events, causing the UI-side security checks to pass on stale state data, while the WebContent process has already moved to a new origin.
Suggested Exploit Steps
- The attacker hosts a malicious website (
attacker.com) and socially engineers the user into saving a malicious bookmarklet in Chrome Bookmarks. - The user navigates to
attacker.comand opens the Bookmarks menu. - Triggered by a UI event (like
blurwhen the menu opens),attacker.cominitiates a renderer-side navigation to a sensitive target (e.g.,mail.google.com). - Immediately,
attacker.combegins flooding the WebKit IPC channel (e.g., rapidly callingpostMessage) to delay state updates to the Chrome UI process. - The navigation to
mail.google.comcommits in the background WebContent process. - The user selects the malicious bookmarklet.
- The Chrome UI process checks if the current URL matches
attacker.com. Because the IPC commit messages are delayed by the flood, the check passes using stale data. - Chrome calls
ExecuteUserJavaScript, which sends an IPC via the unbound[WKWebView evaluateJavaScript:completionHandler:]API. - The WebContent process receives the execution request and runs the malicious script on the currently active document, which is now
mail.google.com, resulting in UXSS.
Note: These are suggested steps; our tooling has not executed a working proof of concept to verify the reliability of the IPC flood in this specific context.
Suggested Fix
The root cause is the use of the legacy, non-frame-bound evaluateJavaScript:completionHandler: API in web::ExecuteJavaScript (specifically the 4-argument overload in ios/web/js_messaging/web_view_js_utils.mm).
The fix should involve migrating calls that execute user-provided JavaScript (like bookmarklets) to use the modern, frame-bound API: [WKWebView evaluateJavaScript:inFrame:inContentWorld:completionHandler:]. By passing a valid WKFrameInfo corresponding to the expected state, WebKit will guarantee that the script execution is dropped if the frame has navigated to a different webpage, effectively neutralizing the race condition.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
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.