CVE-2026-13777
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestAnnotationTextObserverios/web/annotations/annotations_inttest.mm |
modified | |
text_extracted_calls_ios/web/annotations/annotations_inttest.mm |
modified | |
TEST_Fios/web/annotations/annotations_inttest.mm |
modified |
Files Changed
ios/web/annotations/annotations_inttest.mm
Patch
From 49b5cf8e17497e81c6b163632a1f45453661794c Mon Sep 17 00:00:00 2001
From: Olivier Robin <olivierrobin@google.com>
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 <michaeldo@chromium.org>
Reviewed-by: Mike Dougherty <michaeldo@chromium.org>
Auto-Submit: Olivier Robin <olivierrobin@chromium.org>
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,"
Original Bug Report
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.mmios/web/annotations/annotations_text_manager_impl.mmios/chrome/browser/web/model/annotations/annotations_tab_helper.mmios/web/annotations/resources/text_main.tsios/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:
-
Failure to Pass Limits to JavaScript: In
ios/web/annotations/annotations_java_script_feature.mm, theExtractTextfunction receives amaximum_text_lengthparameter (65535) but fails to include it in theparameterslist passed to theannotations.startJavaScript function (lines 44-55). -
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). -
Missing Native-Side Validation: In
ios/web/annotations/annotations_java_script_feature.mm, theScriptMessageReceivedhandler processes theannotations.extractedTextcommand. It extracts thetextstring but does not verify its length before passing it to theAnnotationsTextManagerImpl(lines 131-139). -
Processing in Privileged Process: The unbounded strings are ultimately passed to
ios::provider::ExtractTextAnnotationFromTextviaAnnotationsTabHelper::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
- Host an HTTPS page containing a very large text node (e.g., >100 MB).
- Navigate to this page using Chrome on iOS.
- The
AnnotationsTextManagerImplwill trigger text extraction automatically on page load. - The injected JavaScript will send the full, unbounded text back to the browser process via
sendWebKitMessage. - The browser process will receive and attempt to process the large payload in
AnnotationsTabHelper::OnTextExtractedand the subsequent ThreadPool task.
Recommended Mitigation
- Update
AnnotationsJavaScriptFeature::ExtractTextto pass themaximum_text_lengthto theannotations.startJavaScript function. - Modify the JavaScript extraction logic in
text_extractor.tsto respect the provided length limit. - Implement explicit size and content validation for the
textstring andmetadatafields inAnnotationsJavaScriptFeature::ScriptMessageReceivedbefore 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.
- https://source.chromium.org/chromium/chromium/src/+/main:ios/chrome/browser/web/model/annotations/annotations_tab_helper.mm;l=137
- https://source.chromium.org/chromium/chromium/src/+/main:ios/web/annotations/annotations_java_script_feature.mm;l=131
- https://source.chromium.org/chromium/chromium/src/+/main:ios/web/annotations/annotations_java_script_feature.mm;l=44
- https://source.chromium.org/chromium/chromium/src/+/main:ios/web/annotations/resources/text_extractor.ts;l=122