CVE-2026-13777
Overview
Background
- `AnnotationsJavaScriptFeature`
- The iOS web-layer feature that injects the
text_mainscript into the main frame’s isolated content world and receives itsannotationsscript messages (ios/web/annotations/annotations_java_script_feature.mm:27-34,:172-175). - `ScriptMessageReceived`
- The browser-side handler that parses each page-originated message dictionary (
annotations.extractedText,annotations.decoratingComplete,annotations.onClick) and forwards the values toAnnotationsTextManagerImpl(ios/web/annotations/annotations_java_script_feature.mm:98-170). - `kMaxAnnotationsTextLength`
- A 65535-character limit on extracted page text; before the fix it was a file-static in
annotations_text_manager_impl.mm:19that was passed toExtractTextand then ignored. - `TextExtractor`
- The TypeScript visitor in
text_extractor.tsthat concatenates visible text nodes intopartsandsectionsand advancesindexto build the string that is sent back to native code.
Root Cause Analysis
49b5cf8e1749 was always correctly mapped, but the earlier analysis was written from a truncated copy of its diff that left out the changed product source (annotations_text_manager_impl.mm, text_extractor.ts and text_main.ts) and showed only build/test files. This analysis is written from the complete diff.AnnotationsTextManagerImpl::StartExtractingText passes kMaxAnnotationsTextLength to ExtractText (ios/web/annotations/annotations_text_manager_impl.mm:74-75). However, AnnotationsJavaScriptFeature::ExtractText drops maximum_text_length and calls annotations.start with an empty parameters list (ios/web/annotations/annotations_java_script_feature.mm:44-55, specifically :53-54). On the JS side, the removed visibleTextNode code pushed every non-blank textNode.textContent! and advanced this.index with no cap. The commit message notes that the limit “was removed when the extraction was migrated to viewport extraction”. On receipt, the annotations.extractedText branch only checks that text, seqId and metadata exist (annotations_java_script_feature.mm:131-137) before calling manager->OnTextExtracted(...) (:138-139). That call passes the text to every observer (annotations_text_manager_impl.mm:105-117), so text and metadata strings of any size reached browser-side consumers. The commit 49b5cf8e1749 restores the limit in three places. Native code now rejects text->size() > kMaxAnnotationsTextLength, caps htmlLang/httpContentLanguage at kMaxAnnotationsMetadataLength (256), and returns early on any unexpected top-level or metadata key for all three commands. ExtractText now does parameters.Append(maximum_text_length). start(maxTextLength) passes the limit to TextExtractor, which truncates text and stops adding spacers and section breaks once extractionLimit (maxTextLength - extraCharactersAtEnd * 2) is reached.ScriptMessageReceived) and also passes it to the JS extractor so that well-behaved extraction stays within it.Attack Path
- Serve oversized content
A web page contains very large visible text, per the reporter’s trigger; on successful load
PageLoadedpostsStartExtractingText(annotations_text_manager_impl.mm:85-96). - Unbounded extraction
annotations.startruns without a length argument (annotations_java_script_feature.mm:53-54), soTextExtractorconcatenates all visible text. - Unchecked receipt
The
annotations.extractedTextmessage passes only presence checks (annotations_java_script_feature.mm:131-137) and is sent to observers throughOnTextExtracted(annotations_text_manager_impl.mm:114-116). A compromised web-content process could also send arbitrarytext/metadatadirectly (inferred). - Browser-side processing
Per the reporter, observers such as
AnnotationsTabHelper::OnTextExtractedpass the string toios::provider::ExtractTextAnnotationFromTextand a TFLite classifier in the browser process (inferred; that code is not in the material).
Impact Assessment
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 | |
ifios/web/annotations/annotations_java_script_feature.mm |
modified | |
forios/web/annotations/annotations_java_script_feature.mm |
modified |
Files Changed
ios/web/annotations/annotations_inttest.mmios/web/annotations/annotations_java_script_feature.hios/web/annotations/annotations_java_script_feature.mmios/web/annotations/annotations_text_manager_impl.mmios/web/annotations/resources/text_extractor.tsios/web/annotations/resources/text_extractor_test.tsios/web/annotations/resources/text_main.ts
Audit Directions
- Unused limit parametersLook for native-to-JS calls in
ios/webfeatures that receive a size or count limit and do not forward it in theCallJavaScriptFunctionparameter list, and for script-message handlers that rely on JS-side limits instead of enforcing them at receipt. - Unit mismatch at the boundaryThe JS limit counts UTF-16 code units, but the native check uses
std::string::size()(UTF-8 bytes), so legitimate non-ASCII pages near the limit may be dropped. Check that other handlers apply consistent units and do not accidentally loosen a limit. - Unbounded fields in other commands
annotations.onClickdata/textand thedecoratingCompletecancelledlist are now key-allowlisted but still have no size limit. Audit these and the otherScriptMessagehandlers for the same pattern of untrusted data of any length.
Patch
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,"
+ " metadata: { htmlLang: 'en', httpContentLanguage: '%s' }"
+ "});"
+ "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,
+ RejectMessageWithUnexpectedKeyInExtractedText) {
+ ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+ ASSERT_TRUE(WaitForWebFramesCount(1));
+
+ observer()->Reset();
+
+ NSString* js = @"window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.extractedText',"
+ " text: 'invalid_text',"
+ " seqId: 46,"
+ " metadata: { htmlLang: 'en', httpContentLanguage: 'en' },"
+ " unexpectedKey: 'malicious'"
+ "});"
+ "window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.extractedText',"
+ " text: 'sync_text',"
+ " seqId: 999,"
+ " metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+ "});";
+
+ web::test::ExecuteJavaScriptForFeature(
+ web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+ 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,
+ RejectMessageWithUnexpectedKeyInMetadata) {
+ ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+ ASSERT_TRUE(WaitForWebFramesCount(1));
+
+ observer()->Reset();
+
+ NSString* js = @"window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.extractedText',"
+ " text: 'invalid_text',"
+ " seqId: 47,"
+ " metadata: { htmlLang: 'en', httpContentLanguage: 'en', "
+ "unexpectedKey: 'malicious' }"
+ "});"
+ "window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.extractedText',"
+ " text: 'sync_text',"
+ " seqId: 999,"
+ " metadata: { htmlLang: 'en', httpContentLanguage: 'en' }"
+ "});";
+
+ web::test::ExecuteJavaScriptForFeature(
+ web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+ 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,
+ RejectMessageWithUnexpectedKeyInDecoratingComplete) {
+ ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+ ASSERT_TRUE(WaitForWebFramesCount(1));
+
+ observer()->Reset();
+ int decoration_calls = observer()->decoration_calls();
+
+ NSString* js = @"window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.decoratingComplete',"
+ " annotations: 1,"
+ " successes: 1,"
+ " failures: 0,"
+ " cancelled: [],"
+ " unexpectedKey: 'malicious'"
+ "});"
+ "window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.decoratingComplete',"
+ " annotations: 1,"
+ " successes: 1,"
+ " failures: 0,"
+ " cancelled: []"
+ "});";
+
+ web::test::ExecuteJavaScriptForFeature(
+ web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+ EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
+ return observer()->decoration_calls() > decoration_calls;
+ }));
+ EXPECT_EQ(1, observer()->decoration_calls() - decoration_calls);
+}
+
+TEST_F(AnnotationTextManagerViewportTest,
+ RejectMessageWithUnexpectedKeyInOnClick) {
+ ASSERT_TRUE(LoadHtml("<html><body></body></html>"));
+ ASSERT_TRUE(WaitForWebFramesCount(1));
+
+ observer()->Reset();
+ int clicks = observer()->clicks();
+
+ NSString* js = @"window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.onClick',"
+ " data: 'some_data',"
+ " rect: { x: 0, y: 0, width: 10, height: 10 },"
+ " text: 'some_text',"
+ " cancel: false,"
+ " unexpectedKey: 'malicious'"
+ "});"
+ "window.webkit.messageHandlers.annotations.postMessage({"
+ " command: 'annotations.onClick',"
+ " data: 'some_data',"
+ " rect: { x: 0, y: 0, width: 10, height: 10 },"
+ " text: 'some_text',"
+ " cancel: false"
+ "});";
+
+ web::test::ExecuteJavaScriptForFeature(
+ web_state(), js, AnnotationsJavaScriptFeature::GetInstance());
+
+ EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{
+ return observer()->clicks() > clicks;
+ }));
+ EXPECT_EQ(1, observer()->clicks() - clicks);
+}
+
} // namespace web
diff --git a/ios/web/annotations/annotations_java_script_feature.h b/ios/web/annotations/annotations_java_script_feature.h
index aca0f9b..74464d9 100644
--- a/ios/web/annotations/annotations_java_script_feature.h
+++ b/ios/web/annotations/annotations_java_script_feature.h
@@ -14,6 +14,9 @@
namespace web {
+extern const int kMaxAnnotationsTextLength;
+extern const int kMaxAnnotationsMetadataLength;
+
/**
* Handles JS communication for the annotations feature.
*/
diff --git a/ios/web/annotations/annotations_java_script_feature.mm b/ios/web/annotations/annotations_java_script_feature.mm
index a2938df..e4f8c729 100644
--- a/ios/web/annotations/annotations_java_script_feature.mm
+++ b/ios/web/annotations/annotations_java_script_feature.mm
@@ -24,6 +24,9 @@
namespace web {
+const int kMaxAnnotationsTextLength = 65535;
+const int kMaxAnnotationsMetadataLength = 256;
+
AnnotationsJavaScriptFeature::AnnotationsJavaScriptFeature()
: JavaScriptFeature(
ContentWorld::kIsolatedWorld,
@@ -51,6 +54,7 @@
}
base::ListValue parameters;
+ parameters.Append(maximum_text_length);
CallJavaScriptFunction(frame, "annotations.start", parameters);
}
@@ -129,15 +133,50 @@
}
if (*command == "annotations.extractedText") {
+ for (const auto pair : dict) {
+ const std::string& key = pair.first;
+ if (key != "command" && key != "text" && key != "seqId" &&
+ key != "metadata") {
+ return;
+ }
+ }
const std::string* text = dict.FindString("text");
std::optional<double> seq_id = dict.FindDouble("seqId");
const base::DictValue* metadata = dict.FindDict("metadata");
if (!text || !seq_id || !metadata) {
return;
}
+ for (const auto pair : *metadata) {
+ const std::string& key = pair.first;
+ if (key != "htmlLang" && key != "httpContentLanguage" &&
+ key != "wkNoTelephone" && key != "wkNoEmail" &&
+ key != "wkNoAddress" && key != "wkNoDate" && key != "wkNoUnit") {
+ return;
+ }
+ }
+ if (text->size() > kMaxAnnotationsTextLength) {
+ return;
+ }
+ const std::string* html_lang = metadata->FindString("htmlLang");
+ if (html_lang && html_lang->size() > kMaxAnnotationsMetadataLength) {
+ return;
+ }
+ const std::string* http_content_language =
+ metadata->FindString("httpContentLanguage");
... (file diff truncated)
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