Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Transactions Platform
DescriptionImproper input validation in Transactions Platform
ComponentTransactions Platform
Bug ClassLogic Error
Tracker513790581
Fix commit57bfec561762 (chromium/src) +147/-54
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-03

Changed Functions

FunctionChangeNotes
if
components/autofill/ios/browser/autofill_util.mm
modified
for
components/autofill/ios/browser/autofill_util.mm
modified
TEST_F
components/autofill/ios/browser/autofill_util_unittest.mm
modified

Files Changed

  • components/autofill/ios/browser/autofill_util.h
  • components/autofill/ios/browser/autofill_util.mm
  • components/autofill/ios/browser/autofill_util_unittest.mm
From 57bfec56176222bcdf18fbf64d0c1c34489ef064 Mon Sep 17 00:00:00 2001
From: Christoph Schwering <[email protected]>
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 <[email protected]>
Commit-Queue: Christoph Schwering <[email protected]>
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.