Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Chrome for iOS
DescriptionInappropriate implementation in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker513142464
Fix commit3181bf734666 (chromium/src) +182/-28
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
components/translate/core/browser/resources/translate.js
modified
SimpleURLLoader
components/translate/ios/browser/translate_controller.h
modified
Origin
components/translate/ios/browser/translate_controller.h
modified
WebFrame
components/translate/ios/browser/translate_controller.h
modified

Files Changed

  • components/translate/core/browser/resources/translate.js
  • components/translate/ios/browser/resources/translate_ios.ts
  • components/translate/ios/browser/translate_controller.h
  • components/translate/ios/browser/translate_controller.mm
From 3181bf73466666ec9972a95cbabd62481e6cb86c Mon Sep 17 00:00:00 2001
From: Mike Dougherty <michaeldo@chromium.org>
Date: Tue, 09 Jun 2026 12:43:17 -0700
Subject: [PATCH] [Translate][iOS] Download additional scripts in native code

This CL migrates the loading of additional JS scripts needed by the
translation js library from being downloaded with XMLHttpRequest to
being download on the native side with . It has no intended functional
changes.

Fixed: 513142464
Change-Id: I3ea4b79cbfb98f904b937cae1fbcd5e563848cd5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7897753
Reviewed-by: Olivier Robin <olivierrobin@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644178}
---

diff --git a/components/translate/core/browser/resources/translate.js b/components/translate/core/browser/resources/translate.js
index f657b4cb..71381ea 100644
--- a/components/translate/core/browser/resources/translate.js
+++ b/components/translate/core/browser/resources/translate.js
@@ -121,6 +121,12 @@
    */
   let resultCallback;
 
+  /**
+   * A custom javascript loader to use native side network request.
+   * @type {function(url: string): void}
+   */
+  let customJavaScriptLoader;
+
   function checkLibReady() {
     if (lib.isAvailable()) {
       readyTime = performance.now();
@@ -208,6 +214,16 @@
     },
 
     /**
+     * Setter for customJavaScriptLoader. No op if already set.
+     * @param {function(url: string): void} callback The function to be invoked.
+     */
+    set customJavaScriptLoader(callback) {
+      if (!customJavaScriptLoader) {
+        customJavaScriptLoader = callback;
+      }
+    },
+
+    /**
      * Whether the library is ready.
      * The translate function should only be called when |libReady| is true.
      * @type {boolean}
@@ -395,6 +411,12 @@
         return;
       }
 
+      // Use `customJavaScriptLoader` if set instead of `XMLHttpRequest` below.
+      if (customJavaScriptLoader) {
+        customJavaScriptLoader(url);
+        return;
+      }
+
       const xhr = new XMLHttpRequest();
       xhr.open('GET', url, true);
       xhr.onreadystatechange = function() {
diff --git a/components/translate/ios/browser/resources/translate_ios.ts b/components/translate/ios/browser/resources/translate_ios.ts
index 1d61ff4..55fac758 100644
--- a/components/translate/ios/browser/resources/translate_ios.ts
+++ b/components/translate/ios/browser/resources/translate_ios.ts
@@ -45,6 +45,17 @@
       'translationTime': cr.googleTranslate.translationTime,
     });
   };
+
+  /**
+   * A custom javascript loader to use native side network request.
+   */
+  cr.googleTranslate.customJavaScriptLoader = function(url: string) {
+    sendWebKitMessage('TranslateMessage', {
+      'command': 'loadJavascript',
+      'url': url,
+      'frameId': gCrWeb.getFrameId(),
+    });
+  };
 }
 
 function startTranslation(sourceLanguage: string, targetLanguage: string) {
diff --git a/components/translate/ios/browser/translate_controller.h b/components/translate/ios/browser/translate_controller.h
index 6f35727..dac7d21 100644
--- a/components/translate/ios/browser/translate_controller.h
+++ b/components/translate/ios/browser/translate_controller.h
@@ -7,6 +7,7 @@
 
 #include <iterator>
 #include <memory>
+#include <optional>
 #include <set>
 #include <string>
 
@@ -19,6 +20,14 @@
 #import "ios/web/public/web_state.h"
 #import "ios/web/public/web_state_user_data.h"
 
+namespace network {
+class SimpleURLLoader;
+}
+
+namespace url {
+class Origin;
+}
+
 namespace web {
 class WebFrame;
 }
@@ -70,34 +79,20 @@
                         const std::string& target_language);
 
   // Called when a JavaScript command is received.
-  void OnJavascriptCommandReceived(const base::DictValue& payload);
+  void OnJavascriptCommandReceived(url::Origin security_origin,
+                                   const base::DictValue& payload);
 
  private:
   TranslateController(web::WebState* web_state);
   friend class web::WebStateUserData<TranslateController>;
-
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnJavascriptCommandReceived);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnIFrameJavascriptCommandReceived);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnTranslateScriptReadyTimeoutCalled);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnTranslateScriptReadyCalled);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationSuccess);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationFailure);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, OnTranslateLoadJavascript);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnTranslateSendRequestWithValidCommand);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnTranslateSendRequestWithBadURL);
-  FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest,
-                           OnTranslateSendRequestWithBadMethod);
+  friend class TranslateControllerTest;
 
   // Methods to handle specific JavaScript commands.
   // The command is ignored if `payload` format is unexpected.
   void OnTranslateReady(const base::DictValue& payload);
   void OnTranslateComplete(const base::DictValue& payload);
+  void OnLoadJavascript(url::Origin security_origin,
+                        const base::DictValue& payload);
 
   // The main frame of `web_state_`, if any.
   web::WebFrame* GetMainWebFrame();
@@ -106,6 +101,17 @@
   raw_ptr<web::WebState> web_state_;
 
   base::ObserverList<Observer> observers_;
+
+  // The WebFrame ID for the frame from `web_state_` which the translate script
+  // was last injected.
+  std::optional<std::string> translate_script_injected_frame_id_;
+
+  // Loader used to fetch the translate script.
+  std::unique_ptr<network::SimpleURLLoader> script_loader_;
+
+  // Called when the script is loaded.
+  void OnScriptLoaded(std::string frame_id,
+                      std::optional<std::string> response_body);
 };
 
 }  // namespace translate
diff --git a/components/translate/ios/browser/translate_controller.mm b/components/translate/ios/browser/translate_controller.mm
index 2b7c009..b3e8821 100644
--- a/components/translate/ios/browser/translate_controller.mm
+++ b/components/translate/ios/browser/translate_controller.mm
@@ -5,15 +5,18 @@
 #import "components/translate/ios/browser/translate_controller.h"
 
 #include <cmath>
+#include <optional>
 #include <string_view>
 #include <utility>
 
 #import "base/check_op.h"
+#import "base/debug/dump_without_crashing.h"
 #import "base/functional/bind.h"
 #import "base/functional/callback_helpers.h"
 #import "base/json/string_escape.h"
 #import "base/strings/utf_string_conversions.h"
 #import "base/values.h"
+#import "components/translate/core/browser/translate_download_manager.h"
 #import "components/translate/core/common/translate_util.h"
 #import "components/translate/ios/browser/translate_java_script_feature.h"
 #import "ios/web/public/browser_state.h"
@@ -25,6 +28,7 @@
 #import "net/http/http_status_code.h"
 #import "net/traffic_annotation/network_traffic_annotation.h"
 #import "services/network/public/cpp/resource_request.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/translate/ios/browser/translate_controller_unittest.mm b/components/translate/ios/browser/translate_controller_unittest.mm
index a25d1e7..09aa3c4a 100644
--- a/components/translate/ios/browser/translate_controller_unittest.mm
+++ b/components/translate/ios/browser/translate_controller_unittest.mm
@@ -17,7 +17,7 @@
 #include "ios/web/public/test/web_task_environment.h"
 #include "net/http/http_status_code.h"
 #include "testing/platform_test.h"
-#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace translate {
 
@@ -27,8 +27,6 @@
   TranslateControllerTest()
       : fake_web_state_(std::make_unique<web::FakeWebState>()),
         fake_browser_state_(std::make_unique<web::FakeBrowserState>()),
-        fake_main_frame_(web::FakeWebFrame::Create(/*frame_id=*/"",
-                                                   /*is_main_frame=*/true)),
         error_type_(TranslateErrors::NONE),
         ready_time_(0),
         load_time_(0),
@@ -37,6 +35,7 @@
         on_translate_complete_called_(false) {
     fake_web_state_->SetBrowserState(fake_browser_state_.get());
     auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
+    frames_manager->AddWebFrame(web::FakeWebFrame::CreateMainWebFrame());
     web_frames_manager_ = frames_manager.get();
     web::ContentWorld content_world =
         TranslateJavaScriptFeature::GetInstance()->GetSupportedContentWorld();
@@ -46,6 +45,11 @@
     translate_controller_observation_.Observe(translate_controller());
   }
 
+  void SetUp() override {
+    translate_controller()->translate_script_injected_frame_id_ =
+        web::kMainFakeFrameId;
+  }
+
   // TranslateController::Observer methods.
   void OnTranslateScriptReady(TranslateErrors error_type,
                               double load_time,
@@ -78,7 +82,6 @@
       web::WebTaskEnvironment::MainThreadType::IO};
   std::unique_ptr<web::FakeWebState> fake_web_state_;
   std::unique_ptr<web::FakeBrowserState> fake_browser_state_;
-  std::unique_ptr<web::FakeWebFrame> fake_main_frame_;
   raw_ptr<web::FakeWebFramesManager> web_frames_manager_;
   TranslateErrors error_type_;
   double ready_time_;
@@ -101,7 +104,7 @@
   command.Set("loadTime", .0);
   command.Set("readyTime", .0);
   translate_controller()->OnJavascriptCommandReceived(
-      base::DictValue(std::move(command)));
+      url::Origin(), base::DictValue(std::move(command)));
   EXPECT_TRUE(on_script_ready_called_);
   EXPECT_FALSE(on_translate_complete_called_);
   EXPECT_FALSE(error_type_ == TranslateErrors::NONE);
@@ -120,7 +123,7 @@
   command.Set("loadTime", some_load_time);
   command.Set("readyTime", some_ready_time);
   translate_controller()->OnJavascriptCommandReceived(
-      base::DictValue(std::move(command)));
+      url::Origin(), base::DictValue(std::move(command)));
   EXPECT_TRUE(on_script_ready_called_);
   EXPECT_FALSE(on_translate_complete_called_);
   EXPECT_TRUE(error_type_ == TranslateErrors::NONE);
@@ -141,7 +144,7 @@
   command.Set("pageSourceLanguage", some_source_language);
   command.Set("translationTime", some_translation_time);
   translate_controller()->OnJavascriptCommandReceived(
-      base::DictValue(std::move(command)));
+      url::Origin(), base::DictValue(std::move(command)));
   EXPECT_FALSE(on_script_ready_called_);
   EXPECT_TRUE(on_translate_complete_called_);
   EXPECT_TRUE(error_type_ == TranslateErrors::NONE);
@@ -157,7 +160,7 @@
   command.Set("errorCode",
               static_cast<double>(TranslateErrors::INITIALIZATION_ERROR));
   translate_controller()->OnJavascriptCommandReceived(
-      base::DictValue(std::move(command)));
+      url::Origin(), base::DictValue(std::move(command)));
   EXPECT_FALSE(on_script_ready_called_);
   EXPECT_TRUE(on_translate_complete_called_);
   EXPECT_FALSE(error_type_ == TranslateErrors::NONE);
Loading diff…

Original Bug Report

reported by vm...@google.com

iOS: Potential Isolated World Code Execution in Translate via Service Worker Interception

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The Translate feature on iOS fetches runtime libraries via XMLHttpRequest and executes them within an isolated world. Because iOS isolated worlds share the page’s Service Worker, a malicious site can intercept these requests to execute arbitrary JavaScript in the privileged isolated world context.

Affected files:

  • components/translate/core/browser/resources/translate.js
  • components/translate/ios/browser/translate_java_script_feature.mm
  • components/translate/ios/browser/translate_controller.mm
  • components/translate/ios/browser/ios_translate_driver.mm

Estimated timestamp from git blame: 2024-03-14

Potential Vulnerability: Isolated World Code Execution in Translate for iOS

Root Cause

In components/translate/core/browser/resources/translate.js, the cr.googleTranslate.onLoadJavascript function fetches external scripts using XMLHttpRequest and executes the response using new Function():

onLoadJavascript(url) {
  if (!url.startsWith(securityOrigin)) {
    // ... error handling ...
    return;
  }
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.onreadystatechange = function() {
    if (this.readyState !== this.DONE) return;
    if (this.status !== 200) { /* ... */ return; }
    new Function(this.responseText).call(window);
  };
  xhr.send();
}

On iOS, this Translate bundle is injected into web::ContentWorld::kIsolatedWorld (which maps to WebKit’s WKContentWorld.defaultClientWorld). While this world provides JavaScript namespace isolation, it shares the page’s security origin and its Service Worker registration. Consequently, an XMLHttpRequest issued from this isolated world is treated as a subresource request of the page and is routed through the page-registered Service Worker’s fetch event.

While Desktop Chrome (Blink) mitigates this by setting a SkipServiceWorker flag on isolated-world requests, WKWebView on iOS lacks an equivalent mechanism to bypass Service Workers for requests initiated via JavaScript inside an isolated world.

Potential Attack Scenario

  1. An attacker serves a malicious page that registers a Service Worker.
  2. The Service Worker’s fetch handler intercepts requests to https://translate.googleapis.com/ and returns a malicious JavaScript payload.
  3. The Translate feature is triggered (either automatically or via user gesture).
  4. Chrome injects the Translate bundle into the isolated world.
  5. The script calls onLoadJavascript to fetch its runtime library from translate.googleapis.com.
  6. The XHR is intercepted by the attacker’s Service Worker, which returns the malicious payload.
  7. The payload is executed via new Function() inside the privileged kIsolatedWorld context.

Potential Impact

Successful code execution within kIsolatedWorld allows an attacker to bypass the primary defense-in-depth boundary between web content and the iOS browser process. The attacker gains access to privileged native WKScriptMessageHandler handlers, such as:

  • Password Manager: Access to handlers like PasswordFormSubmitButtonClick to spoof or intercept credential submissions.
  • Autofill: Interaction with form activity tracking and data extraction handlers.
  • Frame Management: Access to the 128-bit unguessable isolated-world frame token via __gCrWeb.getFrameId().

Because these handlers run in the unsandboxed browser process on iOS, this vulnerability represents a significant escalation of privilege from standard web content.

Note: These are potential steps based on code analysis; our current environment does not support running code to verify this with a Proof of Concept.

Suggested Fix

Translate’s iOS implementation should avoid using XMLHttpRequest from within the isolated world to load trusted code. Potential remedies include:

  1. Proxying library loads through the browser process via a dedicated script message, where the browser process can fetch the script using its native network stack (bypassing the renderer’s Service Worker) and then inject it into the isolated world.
  2. Modifying translate.js on iOS to use a mechanism that ensures the requested URL is fetched from a trusted context.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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