Critical chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in iOSWeb
DescriptionInsufficient validation of untrusted input in iOSWeb
ComponentChromium
Bug ClassLogic Error
Tracker513128566
Fix commit49b5cf8e1749 (chromium/src) +361/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TestAnnotationTextObserver
ios/web/annotations/annotations_inttest.mm
modified
text_extracted_calls_
ios/web/annotations/annotations_inttest.mm
modified
TEST_F
ios/web/annotations/annotations_inttest.mm
modified

Files Changed

  • ios/web/annotations/annotations_inttest.mm
From 49b5cf8e17497e81c6b163632a1f45453661794c Mon Sep 17 00:00:00 2001
From: Olivier Robin <[email protected]>
Date: Tue, 19 May 2026 10:34:37 -0700
Subject: [PATCH] Limit Annotation extractions to 65535 characters

Limit was removed when the extraction was migrated to viewport
extraction. Restore the limit to avoid pages to send too much data to
the browser.

Note that the tree is still traversed to maintain the list of
visible/invisible nodes.

Fixed: 513128566
Change-Id: I7bf6ac19706e056ae68a4ecd0120b0cf46313f61
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852462
Commit-Queue: Mike Dougherty <[email protected]>
Reviewed-by: Mike Dougherty <[email protected]>
Auto-Submit: Olivier Robin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1632988}
---

diff --git a/ios/web/annotations/annotations_inttest.mm b/ios/web/annotations/annotations_inttest.mm
index 7198ea3..6fabc5c 100644
--- a/ios/web/annotations/annotations_inttest.mm
+++ b/ios/web/annotations/annotations_inttest.mm
@@ -20,6 +20,7 @@
 #import "ios/web/public/annotations/annotations_text_observer.h"
 #import "ios/web/public/js_messaging/web_frame.h"
 #import "ios/web/public/js_messaging/web_frames_manager.h"
+#import "ios/web/public/test/js_test_util.h"
 #import "ios/web/public/test/web_test_with_web_state.h"
 #import "ios/web/public/web_state.h"
 #import "ios/web/public/web_state_observer.h"
@@ -65,7 +66,11 @@
 class TestAnnotationTextObserver : public AnnotationsTextObserver {
  public:
   TestAnnotationTextObserver()
-      : successes_(0), annotations_(0), clicks_(0), decoration_calls_(0) {}
+      : successes_(0),
+        annotations_(0),
+        clicks_(0),
+        decoration_calls_(0),
+        text_extracted_calls_(0) {}
 
   TestAnnotationTextObserver(const TestAnnotationTextObserver&) = delete;
   TestAnnotationTextObserver& operator=(const TestAnnotationTextObserver&) =
@@ -75,6 +80,7 @@
                        const std::string& text,
                        int seq_id,
                        const base::DictValue& metadata) override {
+    text_extracted_calls_++;
     extracted_text_ = text;
     EXPECT_GE(seq_id, 1);
     seq_id_ = seq_id;
@@ -100,7 +106,10 @@
     click_data_ = data;
   }
 
-  void Reset() { seq_id_ = 0; }
+  void Reset() {
+    seq_id_ = 0;
+    text_extracted_calls_ = 0;
+  }
 
   const std::string& extracted_text() const { return extracted_text_; }
   int successes() const { return successes_; }
@@ -112,10 +121,18 @@
   const std::string& click_data() const { return click_data_; }
   void SetAnnotations(int count) { annotations_ = count; }
   int decoration_calls() const { return decoration_calls_; }
+  int text_extracted_calls() const { return text_extracted_calls_; }
 
  private:
-  std::string extracted_text_, click_data_;
-  int successes_, failures_, annotations_, clicks_, seq_id_, decoration_calls_;
+  std::string extracted_text_;
+  std::string click_data_;
+  int successes_;
+  int failures_;
+  int annotations_;
+  int clicks_;
+  int seq_id_;
+  int decoration_calls_;
+  int text_extracted_calls_;
   base::DictValue metadata_;
 };
 
@@ -755,4 +772,263 @@
   ASSERT_TRUE(observer()->click_data() == "type1-annotation");
 }
 
+TEST_F(AnnotationTextManagerViewportTest, AcceptValidMessage) {
+  ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+  ASSERT_TRUE(WaitForWebFramesCount(1));
+
+  observer()->Reset();
+
+  web::test::ExecuteJavaScriptForFeature(
+      web_state(),
+      @"window.webkit.messageHandlers.annotations.postMessage({"
+       "  command: 'annotations.extractedText',"
+       "  text: 'valid_text',"
+       "  seqId: 42,"
+       "  metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+       "});",
+      AnnotationsJavaScriptFeature::GetInstance());
+
+  // Wait and check that it was received.
+  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
+    return observer()->seq_id() == 42;
+  }));
+  EXPECT_EQ("valid_text", observer()->extracted_text());
+}
+
+TEST_F(AnnotationTextManagerViewportTest, RejectOversizedText) {
+  ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+  ASSERT_TRUE(WaitForWebFramesCount(1));
+
+  observer()->Reset();
+
+  // Create a string of size kMaxAnnotationsTextLength + 1
+  std::string huge_text(kMaxAnnotationsTextLength + 1, 'a');
+  NSString* js =
+      [NSString stringWithFormat:
+                    @"window.webkit.messageHandlers.annotations.postMessage({"
+                     "  command: 'annotations.extractedText',"
+                     "  text: '%s',"
+                     "  seqId: 43,"
+                     "  metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+                     "});"
+                     "window.webkit.messageHandlers.annotations.postMessage({"
+                     "  command: 'annotations.extractedText',"
+                     "  text: 'sync_text',"
+                     "  seqId: 999,"
+                     "  metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+                     "});",
+                    huge_text.c_str()];
+
+  web::test::ExecuteJavaScriptForFeature(
+      web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+  // Once the second (valid) message is processed, we are guaranteed that the
+  // first (invalid) message was already processed and discarded.
+  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
+    return observer()->seq_id() == 999;
+  }));
+  EXPECT_EQ("sync_text", observer()->extracted_text());
+  EXPECT_EQ(1, observer()->text_extracted_calls());
+}
+
+TEST_F(AnnotationTextManagerViewportTest, RejectOversizedHtmlLang) {
+  ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+  ASSERT_TRUE(WaitForWebFramesCount(1));
+
+  observer()->Reset();
+
+  // Create a string of size kMaxAnnotationsMetadataLength + 1
+  std::string huge_lang(kMaxAnnotationsMetadataLength + 1, 'a');
+  NSString* js =
+      [NSString stringWithFormat:
+                    @"window.webkit.messageHandlers.annotations.postMessage({"
+                     "  command: 'annotations.extractedText',"
+                     "  text: 'valid_text',"
+                     "  seqId: 44,"
+                     "  metadata: { htmlLang: '%s', httpContentLanguage: 'en' }"
+                     "});"
+                     "window.webkit.messageHandlers.annotations.postMessage({"
+                     "  command: 'annotations.extractedText',"
+                     "  text: 'sync_text',"
+                     "  seqId: 999,"
+                     "  metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+                     "});",
+                    huge_lang.c_str()];
+
+  web::test::ExecuteJavaScriptForFeature(
+      web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+  // Once the second (valid) message is processed, we are guaranteed that the
+  // first (invalid) message was already processed and discarded.
+  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
+    return observer()->seq_id() == 999;
+  }));
+  EXPECT_EQ("sync_text", observer()->extracted_text());
+  EXPECT_EQ(1, observer()->text_extracted_calls());
+}
+
+TEST_F(AnnotationTextManagerViewportTest, RejectOversizedHttpContentLanguage) {
+  ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+  ASSERT_TRUE(WaitForWebFramesCount(1));
+
+  observer()->Reset();
+
+  // Create a string of size kMaxAnnotationsMetadataLength + 1
+  std::string huge_lang(kMaxAnnotationsMetadataLength + 1, 'a');
+  NSString* js =
+      [NSString stringWithFormat:
+                    @"window.webkit.messageHandlers.annotations.postMessage({"
+                     "  command: 'annotations.extractedText',"
+                     "  text: 'valid_text',"
+                     "  seqId: 45,"
Loading diff…

Original Bug Report

reported by [email protected]

Potential Missing Validation for Web-Controlled Text Extraction in iOS Annotations

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 iOS Annotations feature fails to enforce intended length limits for text extracted from web pages and sent to the browser process. A malicious website can bypass the 64 KiB limit, sending unbounded strings from a sandboxed WebContent process to a privileged browser process. This data is subsequently processed by a TFLite-based classifier, potentially leading to memory corruption or resource exhaustion.

Affected files:

  • ios/web/annotations/annotations_java_script_feature.mm
  • ios/web/annotations/annotations_text_manager_impl.mm
  • ios/chrome/browser/web/model/annotations/annotations_tab_helper.mm
  • ios/web/annotations/resources/text_main.ts
  • ios/web/annotations/resources/text_extractor.ts

Estimated timestamp from git blame: 2024-01-17

Description

A potential validation gap in the iOS Annotations pipeline allows a malicious webpage to send unbounded text and metadata from the web content (WKWebView) to the browser process. While a 64 KiB limit (kMaxAnnotationsTextLength) is defined in the native code, it is not enforced during extraction or upon receipt. This results in arbitrarily large strings reaching a TFLite-based classifier running in the privileged iOS browser process.

Root Cause Analysis

The vulnerability stems from multiple missing validation steps in the communication pipeline between the injected JavaScript and the native browser code:

  1. Failure to Pass Limits to JavaScript: In ios/web/annotations/annotations_java_script_feature.mm, the ExtractText function receives a maximum_text_length parameter (65535) but fails to include it in the parameters list passed to the annotations.start JavaScript function (lines 44-55).

  2. Unrestricted JavaScript Extraction: In ios/web/annotations/resources/text_extractor.ts, the extraction logic collects and concatenates text nodes without enforcing a cumulative length limit (lines 122-128).

  3. Missing Native-Side Validation: In ios/web/annotations/annotations_java_script_feature.mm, the ScriptMessageReceived handler processes the annotations.extractedText command. It extracts the text string but does not verify its length before passing it to the AnnotationsTextManagerImpl (lines 131-139).

  4. Processing in Privileged Process: The unbounded strings are ultimately passed to ios::provider::ExtractTextAnnotationFromText via AnnotationsTabHelper::OnTextExtracted (ios/chrome/browser/web/model/annotations/annotations_tab_helper.mm:137). This function executes in the main application process (the browser process) and utilizes a complex TFLite model for text classification.

Potential Impact

On iOS, although all applications are sandboxed by the OS, the browser process acts as the privileged coordinator for the application. Exposing this process to unbounded, web-controlled input increases the attack surface:

  • Resource Exhaustion: A malicious page could cause the browser process to consume excessive memory by sending very large text nodes, potentially leading to an out-of-memory (OOM) condition.
  • Memory Corruption: The unbounded strings are processed by the in-process annotator. Any vulnerability in the tokenizer or the TFLite inference engine could be leveraged to achieve memory corruption within the browser process tier.

Suggested Potential Steps to Trigger

  1. Host an HTTPS page containing a very large text node (e.g., >100 MB).
  2. Navigate to this page using Chrome on iOS.
  3. The AnnotationsTextManagerImpl will trigger text extraction automatically on page load.
  4. The injected JavaScript will send the full, unbounded text back to the browser process via sendWebKitMessage.
  5. The browser process will receive and attempt to process the large payload in AnnotationsTabHelper::OnTextExtracted and the subsequent ThreadPool task.
  1. Update AnnotationsJavaScriptFeature::ExtractText to pass the maximum_text_length to the annotations.start JavaScript function.
  2. Modify the JavaScript extraction logic in text_extractor.ts to respect the provided length limit.
  3. Implement explicit size and content validation for the text string and metadata fields in AnnotationsJavaScriptFeature::ScriptMessageReceived before further processing.

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