Chrome · Transactions Platform
CVE-2026-85047
Logic Error in Transactions Platform
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/ios/browser/autofill_util.mm |
modified | |
forcomponents/autofill/ios/browser/autofill_util.mm |
modified | |
TEST_Fcomponents/autofill/ios/browser/autofill_util_unittest.mm |
modified |
Files Changed
components/autofill/ios/browser/autofill_util.hcomponents/autofill/ios/browser/autofill_util.mmcomponents/autofill/ios/browser/autofill_util_unittest.mm
Patch
From 57bfec56176222bcdf18fbf64d0c1c34489ef064 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <schwering@google.com>
Date: Tue, 30 Jun 2026 14:39:30 -0700
Subject: [PATCH] [Autofill] Validate child frame predecessor index on Bling
Bug: 513790581
Change-Id: If56c123329c002de88317915b7aba32e0337e27a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8004591
Reviewed-by: Vincent Boisselle <vincb@google.com>
Commit-Queue: Christoph Schwering <schwering@google.com>
Cr-Commit-Position: refs/heads/main@{#1655061}
---
diff --git a/components/autofill/ios/browser/autofill_util.h b/components/autofill/ios/browser/autofill_util.h
index 9e3fd331..eff9c0f0 100644
--- a/components/autofill/ios/browser/autofill_util.h
+++ b/components/autofill/ios/browser/autofill_util.h
@@ -134,12 +134,6 @@
const FieldDataManager& field_data_manager,
FormFieldData* field_data);
-// Extracts a single child frame's data from the JSON dictionary into a
-// FrameTokenWithPredecessor object. Returns false if the data could not be
-// extracted.
-bool ExtractRemoteFrameToken(const base::DictValue& frame_data,
- FrameTokenWithPredecessor* token_with_predecessor);
-
typedef base::OnceCallback<void(const base::Value*)> JavaScriptResultCallback;
// Creates a callback for a string JS function return type.
@@ -196,6 +190,13 @@
// works.
web::WebFramesManager* GetWebFramesManagerForAutofill(web::WebState* web_state);
+std::vector<FrameTokenWithPredecessor> ExtractChildFramesForTest(
+ const base::DictValue& form);
+
+bool ExtractRemoteFrameTokenForTest(
+ const base::DictValue& frame_data,
+ FrameTokenWithPredecessor* token_with_predecessor);
+
} // namespace autofill
#endif // COMPONENTS_AUTOFILL_IOS_BROWSER_AUTOFILL_UTIL_H_
diff --git a/components/autofill/ios/browser/autofill_util.mm b/components/autofill/ios/browser/autofill_util.mm
index 3588dca..b3065cf 100644
--- a/components/autofill/ios/browser/autofill_util.mm
+++ b/components/autofill/ios/browser/autofill_util.mm
@@ -70,6 +70,59 @@
std::move(callback).Run(result);
}
+// Extracts a single child frame's data from the JSON dictionary into a
+// FrameTokenWithPredecessor object. Returns false if the data could not be
+// extracted.
+bool ExtractRemoteFrameToken(
+ const base::DictValue& frame_data,
+ FrameTokenWithPredecessor* token_with_predecessor) {
+ const std::string* frame_id = frame_data.FindString("token");
+ if (!frame_id) {
+ return false;
+ }
+
+ std::optional<base::UnguessableToken> token =
+ DeserializeJavaScriptFrameId(*frame_id);
+ if (!token) {
+ return false;
+ }
+
+ const std::optional<int> predecessor =
+ frame_data.FindDouble("predecessor").transform([](double x) {
+ return base::saturated_cast<int>(x);
+ });
+ if (!predecessor || *predecessor < -1) {
+ return false;
+ }
+
+ token_with_predecessor->token = RemoteFrameToken(*token);
+ token_with_predecessor->predecessor = *predecessor;
+ return true;
+}
+
+// Extracts the child frames from the JSON dictionary. Returns an empty vector
+// if the data could not be extracted.
+std::vector<FrameTokenWithPredecessor> ExtractChildFrames(
+ const base::DictValue& form) {
+ std::vector<FrameTokenWithPredecessor> child_frames;
+ if (const base::ListValue* child_frames_list =
+ form.FindList("child_frames")) {
+ for (const auto& frame_dict : *child_frames_list) {
+ if (FrameTokenWithPredecessor token;
+ frame_dict.is_dict() &&
+ ExtractRemoteFrameToken(frame_dict.GetDict(), &token)) {
+ child_frames.push_back(std::move(token));
+ }
+ }
+ }
+ // Validate that the child frames occur in ascending order.
+ if (!std::ranges::is_sorted(child_frames, {},
+ &FrameTokenWithPredecessor::predecessor)) {
+ child_frames.clear();
+ }
+ return child_frames;
+}
+
} // namespace
bool IsContextSecureForWebState(web::WebState* web_state) {
@@ -257,19 +310,7 @@
}
if (include_frame_metadata) {
- // Child frame tokens, optional.
- if (const base::ListValue* child_frames_list =
- form.FindList("child_frames")) {
- std::vector<FrameTokenWithPredecessor> child_frames;
- for (const auto& frame_dict : *child_frames_list) {
- FrameTokenWithPredecessor token;
- if (frame_dict.is_dict() &&
- ExtractRemoteFrameToken(frame_dict.GetDict(), &token)) {
- child_frames.push_back(std::move(token));
- }
- }
- form_data.set_child_frames(std::move(child_frames));
- }
+ form_data.set_child_frames(ExtractChildFrames(form));
}
// Field list (mandatory) is extracted.
@@ -436,31 +477,6 @@
return true;
}
-bool ExtractRemoteFrameToken(
- const base::DictValue& frame_data,
- FrameTokenWithPredecessor* token_with_predecessor) {
- const std::string* frame_id = frame_data.FindString("token");
- if (!frame_id) {
- return false;
- }
-
- std::optional<base::UnguessableToken> token =
- DeserializeJavaScriptFrameId(*frame_id);
- if (!token) {
- return false;
- }
-
- const std::optional<double> predecessor =
- frame_data.FindDouble("predecessor");
- if (!predecessor) {
- return false;
- }
-
- token_with_predecessor->token = RemoteFrameToken(*token);
- token_with_predecessor->predecessor = *predecessor;
- return true;
-}
-
JavaScriptResultCallback CreateStringCallback(
void (^completionHandler)(NSString*)) {
return CreateStringCallback(base::BindOnce(completionHandler));
@@ -531,4 +547,15 @@
ContentWorldForAutofillJavascriptFeatures());
}
+std::vector<FrameTokenWithPredecessor> ExtractChildFramesForTest( // IN-TEST
+ const base::DictValue& form) {
+ return ExtractChildFrames(form);
+}
+
+bool ExtractRemoteFrameTokenForTest( // IN-TEST
+ const base::DictValue& frame_data,
+ FrameTokenWithPredecessor* token_with_predecessor) {
+ return ExtractRemoteFrameToken(frame_data, token_with_predecessor);
+}
+
} // namespace autofill
diff --git a/components/autofill/ios/browser/autofill_util_unittest.mm b/components/autofill/ios/browser/autofill_util_unittest.mm
index 6dfb3af0..019909b 100644
--- a/components/autofill/ios/browser/autofill_util_unittest.mm
+++ b/components/autofill/ios/browser/autofill_util_unittest.mm
@@ -16,6 +16,8 @@
#import "components/autofill/core/common/form_field_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/common/features.h"
+#import "testing/gmock/include/gmock/gmock.h"
+#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "url/gurl.h"
#import "url/origin.h"
@@ -30,6 +32,8 @@
using ::autofill::ExtractIDs;
using ::autofill::FieldRendererId;
using ::base::ASCIIToUTF16;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
TEST_F(AutofillUtilTest, ExtractFormData_FullUrl) {
base::test::ScopedFeatureList scoped_feature_list;
@@ -179,17 +183,16 @@
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/autofill/ios/browser/autofill_util_unittest.mm b/components/autofill/ios/browser/autofill_util_unittest.mm
index 6dfb3af0..019909b 100644
--- a/components/autofill/ios/browser/autofill_util_unittest.mm
+++ b/components/autofill/ios/browser/autofill_util_unittest.mm
@@ -16,6 +16,8 @@
#import "components/autofill/core/common/form_field_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/common/features.h"
+#import "testing/gmock/include/gmock/gmock.h"
+#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "url/gurl.h"
#import "url/origin.h"
@@ -30,6 +32,8 @@
using ::autofill::ExtractIDs;
using ::autofill::FieldRendererId;
using ::base::ASCIIToUTF16;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
TEST_F(AutofillUtilTest, ExtractFormData_FullUrl) {
base::test::ScopedFeatureList scoped_feature_list;
@@ -179,17 +183,16 @@
EXPECT_FALSE(token.has_value());
}
-// Test that the properties mask is extracted from the form field data.
+// Test that the child frames is extracted from the form field data.
TEST_F(AutofillUtilTest, ExtractRemoteFrameToken) {
- base::DictValue remote_frame_token_dict;
- remote_frame_token_dict.Set("token",
- base::Value("beefbeefbeefbeefcafecafecafecafe"));
- remote_frame_token_dict.Set("predecessor", base::Value(64));
+ base::DictValue wellformed1;
+ wellformed1.Set("token", base::Value("beefbeefbeefbeefcafecafecafecafe"));
+ wellformed1.Set("predecessor", base::Value(64));
autofill::FrameTokenWithPredecessor token_with_predecessor;
- ASSERT_TRUE(ExtractRemoteFrameToken(remote_frame_token_dict,
- &token_with_predecessor));
+ ASSERT_TRUE(
+ ExtractRemoteFrameTokenForTest(wellformed1, &token_with_predecessor));
EXPECT_EQ(base::ToLowerASCII(std::get<autofill::RemoteFrameToken>(
token_with_predecessor.token)
.ToString()),
@@ -198,16 +201,78 @@
base::DictValue malformed1;
malformed1.Set("garbage", base::Value("garbage"));
- EXPECT_FALSE(ExtractRemoteFrameToken(malformed1, &token_with_predecessor));
+ EXPECT_FALSE(
+ ExtractRemoteFrameTokenForTest(malformed1, &token_with_predecessor));
base::DictValue malformed2;
malformed2.Set("token", base::Value("garbage"));
- EXPECT_FALSE(ExtractRemoteFrameToken(malformed2, &token_with_predecessor));
+ EXPECT_FALSE(
+ ExtractRemoteFrameTokenForTest(malformed2, &token_with_predecessor));
base::DictValue malformed3;
malformed3.Set("token", base::Value("beefbeefbeefbeefcafecafecafecafe"));
malformed3.Set("predecessor", base::Value("garbage"));
- EXPECT_FALSE(ExtractRemoteFrameToken(malformed3, &token_with_predecessor));
+ EXPECT_FALSE(
+ ExtractRemoteFrameTokenForTest(malformed3, &token_with_predecessor));
+
+ // Test that -1 is the only negative number supported for `predecessor`.
+ base::DictValue wellformed2 = wellformed1.Clone();
+ wellformed2.Set("predecessor", base::Value(-1));
+ EXPECT_TRUE(
+ ExtractRemoteFrameTokenForTest(wellformed2, &token_with_predecessor));
+
+ base::DictValue malformed4 = wellformed1.Clone();
+ malformed4.Set("predecessor", base::Value(-5));
+ EXPECT_FALSE(
+ ExtractRemoteFrameTokenForTest(malformed4, &token_with_predecessor));
+}
+
+// Tests that ExtractChildFrames() only accepts predecessors that in ascending
+// order.
+TEST_F(AutofillUtilTest, ExtractChildFrames_PredecessorsMustBeSorted) {
+ auto create_child = [](std::string token, int predecessor) {
+ base::DictValue child;
+ child.Set("token", base::Value(std::move(token)));
+ child.Set("predecessor", base::Value(predecessor));
+ return child;
+ };
+ auto create_children = [](auto&&... children) {
+ base::ListValue list;
+ (list.Append(std::move(children)), ...);
+ return list;
+ };
+
+ base::DictValue form;
+ EXPECT_THAT(ExtractChildFramesForTest(form), IsEmpty());
+
+ form.Set("child_frames", base::ListValue());
+ EXPECT_THAT(ExtractChildFramesForTest(form), IsEmpty());
+
+ form.Set("child_frames", create_children());
+ EXPECT_THAT(ExtractChildFramesForTest(form), IsEmpty());
+
+ form.Set("child_frames", create_children(create_child(
+ "aeefbeefbeefbeefcafecafecafecafe", 12)));
+ EXPECT_THAT(ExtractChildFramesForTest(form), SizeIs(1));
+
+ form.Set(
+ "child_frames",
+ create_children(create_child("aeefbeefbeefbeefcafecafecafecafe", 12),
+ create_child("beefbeefbeefbeefcafecafecafecafe", 23)));
+ EXPECT_THAT(ExtractChildFramesForTest(form), SizeIs(2));
+
+ form.Set(
+ "child_frames",
+ create_children(create_child("aeefbeefbeefbeefcafecafecafecafe", -1),
+ create_child("beefbeefbeefbeefcafecafecafecafe", 12),
+ create_child("ceefbeefbeefbeefcafecafecafecafe", 23)));
+ EXPECT_THAT(ExtractChildFramesForTest(form), SizeIs(3));
+
+ form.Set(
+ "child_frames",
+ create_children(create_child("aeefbeefbeefbeefcafecafecafecafe", 99),
+ create_child("beefbeefbeefbeefcafecafecafecafe", 1)));
+ EXPECT_THAT(ExtractChildFramesForTest(form), IsEmpty());
}
} // namespace
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page