CVE-2026-84327
Overview
Background
- Autofill OTP detection
- Chromium’s Autofill subsystem that scans parsed forms for one-time-code inputs so surfaces like the keyboard accessory can offer to fill them.
- `ONE_TIME_CODE`
- the Autofill field type assigned to inputs recognized as one-time passcode entry fields.
- TLD+1
- the registrable domain (top-level domain plus one label, e.g.
example.com), the granularity Chromium uses to decide whether two origins are “same-site”. - `net::registry_controlled_domains::SameDomainOrHost`
- a net utility that returns whether two origins share the same registry-controlled domain (TLD+1).
Root Cause Analysis
The private helper IsOtpForm in otp_field_detector.cc classified a form as an OTP form whenever any focusable field carried the ONE_TIME_CODE type, using std::ranges::any_of with no check on the origin of that field. Because a form’s fields can live inside cross-origin iframes, an attacker-controlled iframe embedding an OTP field could cause the OtpFieldDetector to notify UI surfaces (such as the keyboard accessory) about an OTP field that did not belong to the top-level site. The violated invariant is that an OTP prompt on the main tab should only be driven by fields same-site with the top-level frame, so that a cross-origin embedded frame cannot solicit the user’s one-time code.
The fix iterates the fields explicitly and, when kAutofillRestrictOtpToSameTldPlusOne is enabled, compares each ONE_TIME_CODE field’s origin() against form.main_frame_origin() via SameDomainOrHost, dropping the entire form if any OTP field is cross-site. This works because it re-establishes the origin binding between the detected OTP field and the frame the user is actually interacting with.
Attack Path
- Embed a hostile iframe
A malicious site frames content (or a legitimate site embeds a cross-origin frame) that renders an input parsed by Autofill as a
ONE_TIME_CODEfield. - Trigger detection
When Autofill processes cached forms,
IsOtpFormsees the focusableONE_TIME_CODEfield and classifies the form as an OTP form regardless of its origin. - UI notification fires
OtpFieldDetectorcallsAddFormAndNotifyIfNecessary, so surfaces like the keyboard accessory present an OTP-related affordance for the cross-origin field. - User misdirection The user, trusting the top-level site, may direct or expose their one-time code to a field that actually belongs to a different-origin embedded frame.
Impact Assessment
kAutofillRestrictOtpToSameTldPlusOne feature flag.Changed Functions
| Function | Change | Notes |
|---|---|---|
OtpFieldDetectorcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc |
modified | |
ifcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc |
modified | |
AutofillClientcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h |
modified | |
FormStructurecomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h |
modified | |
FormStructurecomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager.h |
modified | |
ifcomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc |
modified | |
BrowserAutofillManagercomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h |
modified | |
FormStructurecomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h |
modified | |
LogBuffercomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h |
modified | |
TESTcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc |
modified |
Files Changed
components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cccomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.hcomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cccomponents/autofill/core/browser/integrators/one_time_tokens/mock_otp_manager.hcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.hcomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager.hcomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cccomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cccomponents/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cccomponents/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cccomponents/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cc
Audit Directions
- Field-type-only trustFlag any Autofill or form-handling logic that keys UI or fill decisions on a field’s type or attributes without checking the field’s
origin()against the relevant frame origin. - Cross-frame form aggregationReview code that iterates
form.fields()treating a form as a single-origin unit, since Autofill forms may span cross-origin iframes and need per-field origin checks. - Same-site comparisonsAudit uses of
SameDomainOrHostand origin comparisons to confirm the correct pair of origins is compared and that theINCLUDE_PRIVATE_REGISTRIESpolicy matches the security intent.
Patch
From 096c31af75365c2a3ef7c1854d71045d2ce5ec89 Mon Sep 17 00:00:00 2001
From: Matthias Koerber <koerber@google.com>
Date: Mon, 17 Aug 2026 08:22:43 -0700
Subject: [PATCH] Apply TLD+1 matching to SMS OTP
This change ensures that kAutofillRestrictOtpToSameTldPlusOne is respected in OtpManagerImpl before fetching SMS OTPs.
Specifically:
1. OtpFieldDetector::IsOtpForm is made public static so it can be shared.
2. OtpManagerImpl now calls OtpFieldDetector::IsOtpForm in OnFieldTypesDetermined and GetOtpSuggestions before subscribing/fetching.
3. OtpManager::GetOtpSuggestions interface is updated to accept FormStructure so that implementations can check if the form is valid.
TAG=agy
CONV=76fbbabd-2d8a-44db-ad4b-ddfaeda7296e
Fixed: 498725213
Change-Id: I95cc51debaec1f187c51f99911e2a3dc99f46209
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8232118
Reviewed-by: Jihad Hanna <jihadghanna@google.com>
Commit-Queue: Matthias Körber <koerber@google.com>
Cr-Commit-Position: refs/heads/main@{#1680560}
---
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
index 6c3524c..5aa0c9f 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc
@@ -1420,9 +1420,10 @@
// still need to offer Autocomplete.
// TODO(crbug.com/433224307): Consider early returning here when the cache
// starts storing all forms and fields.
- AutofillField* autofill_field =
- FindMutableFormAndField(form.global_id(), field.global_id())
- .autofill_field;
+ MutableFormAndField form_and_field =
+ FindMutableFormAndField(form.global_id(), field.global_id());
+ FormStructure* form_structure = form_and_field.form_structure;
+ AutofillField* autofill_field = form_and_field.autofill_field;
auto generate_suggestions_and_maybe_show_ui_phase2 = base::BindOnce(
&BrowserAutofillManager::GenerateSuggestionsAndMaybeShowUIPhase2,
@@ -1432,14 +1433,14 @@
// `otp_manager_` may not be instantiated on all platforms. If a focused field
// is not classified, `autofill_field` is null but the field may be filled by
// autocomplete.
- if (otp_manager_ && autofill_field &&
+ if (otp_manager_ && form_structure && autofill_field &&
autofill_field->Type().GetTypes().contains(ONE_TIME_CODE)) {
if (!client().IsContextSecure()) {
std::move(generate_suggestions_and_maybe_show_ui_phase2).Run({});
return;
}
otp_manager_->GetOtpSuggestions(
- field.origin(),
+ *form_structure, field.origin(),
std::move(generate_suggestions_and_maybe_show_ui_phase2));
return;
}
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/mock_otp_manager.h b/components/autofill/core/browser/integrators/one_time_tokens/mock_otp_manager.h
index db1e8b3..b89c03f 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/mock_otp_manager.h
+++ b/components/autofill/core/browser/integrators/one_time_tokens/mock_otp_manager.h
@@ -21,7 +21,8 @@
MOCK_METHOD(void,
GetOtpSuggestions,
- (const url::Origin& origin,
+ (const FormStructure& form,
+ const url::Origin& origin,
OtpManager::GetOtpSuggestionsCallback callback),
(override));
};
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc
index 9e382bb0..42cca74 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.cc
@@ -22,12 +22,7 @@
namespace autofill {
-namespace {
-
-// Returns true if the form contains at least one ONE_TIME_CODE field and
-// all ONE_TIME_CODE fields in the form are same-site with the main frame's
-// origin.
-[[nodiscard]] bool IsOtpForm(const FormStructure& form) {
+bool OtpFieldDetector::IsOtpForm(const FormStructure& form) {
const bool restrict_to_same_tld = base::FeatureList::IsEnabled(
features::kAutofillRestrictOtpToSameTldPlusOne);
@@ -54,17 +49,6 @@
return has_otp_field;
}
-// Returns true if the `form_id` in `manager` contains at least one ONE_TIME_CODE
-// field and all ONE_TIME_CODE fields in the form are same-site with the main
-// frame's origin.
-[[nodiscard]] bool IsOtpForm(const AutofillManager& manager,
- FormGlobalId form_id) {
- const FormStructure* form_structure = manager.FindCachedFormById(form_id);
- return form_structure && IsOtpForm(*form_structure);
-}
-
-} // namespace
-
OtpFieldDetector::OtpFieldDetector(AutofillClient* client) {
if (client) {
autofill_manager_observation_.Observe(client);
@@ -101,7 +85,8 @@
FormGlobalId form,
FieldTypeSource source,
bool small_forms_were_parsed) {
- if (IsOtpForm(manager, form)) {
+ const FormStructure* form_structure = manager.FindCachedFormById(form);
+ if (form_structure && IsOtpForm(*form_structure)) {
AddFormAndNotifyIfNecessary(form);
} else {
RemoveFormAndNotifyIfNecessary(form);
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h
index dabc860..5d1f9da 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h
@@ -16,6 +16,7 @@
namespace autofill {
class AutofillClient;
+class FormStructure;
// Utility class to observe if a WebContents (with all of its frames)
// - moves from having 0 OTP fields to >0 OTP fields (only focusable fields are
@@ -56,6 +57,12 @@
// one OTP field.
bool IsOtpFieldPresent() const;
+ // Returns true if the `form` contains at least one focusable `ONE_TIME_CODE`
+ // field. If `kAutofillRestrictOtpToSameTldPlusOne` is enabled, also requires
+ // that all focusable `ONE_TIME_CODE` fields in the `form` are same-site with
+ // the main frame's origin.
+ [[nodiscard]] static bool IsOtpForm(const FormStructure& form);
+
// AutofillManager::Observer:
// `OnFieldTypesDetermined` informs us incrementally about the discovery of
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager.h b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager.h
index 81bae8e..837e5cf 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager.h
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager.h
@@ -13,6 +13,8 @@
namespace autofill {
+class FormStructure;
+
// The OtpManager helps the BrowserAutofillManager filling OTPs into webforms.
//
// One instance per frame, owned by the BrowserAutofillManager.
@@ -35,7 +37,8 @@
// an OTP arrived. That's bad for cooperative behavior. Instead the callback
// should return immediately when no OTPs are cached but the Autofill UI
// should be updated once OTPs arrive.
- virtual void GetOtpSuggestions(const url::Origin& origin,
+ virtual void GetOtpSuggestions(const FormStructure& form,
+ const url::Origin& origin,
GetOtpSuggestionsCallback callback) = 0;
};
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
index 233cdd7..3edf6e2 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
@@ -24,6 +24,7 @@
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_client.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
+#include "components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector.h"
#include "components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h"
#include "components/autofill/core/browser/logging/log_manager.h"
#include "components/autofill/core/common/autofill_internals/log_message.h"
@@ -67,8 +68,14 @@
OtpManagerImpl::~OtpManagerImpl() = default;
void OtpManagerImpl::GetOtpSuggestions(
+ const FormStructure& form,
const url::Origin& origin,
OtpManagerImpl::GetOtpSuggestionsCallback callback) {
+ if (!OtpFieldDetector::IsOtpForm(form)) {
+ std::move(callback).Run({});
+ return;
+ }
+
// TODO(crbug.com/415273270) This is just a hack to prepopulate the OTPs in
// case no real backend is triggered. The feature definition should migrate to
// autofill.
@@ -124,11 +131,7 @@
return;
}
- const bool form_contains_otp_field = std::ranges::any_of(
- form->fields(), [](const std::unique_ptr<AutofillField>& field) {
- return field->Type().GetTypes().contains(ONE_TIME_CODE);
- });
- if (!form_contains_otp_field) {
+ if (!OtpFieldDetector::IsOtpForm(*form)) {
return;
}
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
index 9bba6f4..65a092f 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
@@ -28,6 +28,7 @@
namespace autofill {
class BrowserAutofillManager;
+class FormStructure;
class LogBuffer;
// Used for histograms. Do not reorder.
@@ -60,7 +61,8 @@
// OtpManager:
// Returns any cached OTPs (if they exist) and renews a subscription so that
// incoming OTPs can be reported.
- void GetOtpSuggestions(const url::Origin& origin,
+ void GetOtpSuggestions(const FormStructure& form,
+ const url::Origin& origin,
GetOtpSuggestionsCallback callback) override;
// AutofillManager::Observer:
diff --git a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cc b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cc
index 8126d934..0f9c0f4 100644
--- a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cc
+++ b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cc
@@ -86,7 +86,7 @@
}
otp_manager_->GetOtpSuggestions(
- trigger_field.origin(),
+ *form_structure, trigger_field.origin(),
base::BindOnce(&OtpSuggestionGenerator::OnOtpReturned,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
index 35885d10..e2f9f85 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -7157,7 +7157,7 @@
// suggests that.
const std::vector<std::string> otp_values = {"123456"};
EXPECT_CALL(otp_manager(), GetOtpSuggestions)
- .WillOnce(RunOnceCallback<1>(otp_values));
+ .WillOnce(RunOnceCallback<2>(otp_values));
OnAskForValuesToFill(form, form.fields()[0]);
EXPECT_TRUE(external_delegate()->on_suggestions_returned_seen());
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
index df8b675b..6032d06 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
@@ -11,6 +11,8 @@
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
+#include "components/autofill/core/browser/autofill_type.h"
+#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_manager_test_api.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
@@ -421,4 +423,135 @@
OtpFieldDetectorAutofillManagerObserverTest,
testing::Bool());
+namespace {
+
+// Helper to construct a FormStructure with a single field for testing
+// `IsOtpForm`.
+std::unique_ptr<FormStructure> CreateFormWithField(
+ const url::Origin& main_frame_origin,
+ const url::Origin& field_origin,
+ FieldType type,
+ bool is_focusable = true) {
+ FormData form;
+ form.set_main_frame_origin(main_frame_origin);
+ FormFieldData field;
+ field.set_origin(field_origin);
+ field.set_is_focusable(is_focusable);
+ form.set_fields({field});
+
+ auto form_structure = std::make_unique<FormStructure>(form);
+ if (form_structure->fields().empty()) {
+ return nullptr;
+ }
+ form_structure->field(0)->SetTypeTo(AutofillType(type), std::nullopt);
+ return form_structure;
+}
+
+} // namespace
+
+// Tests that `IsOtpForm` returns false when there are no OTP fields in the
+// form.
+TEST(OtpFieldDetectorIsOtpFormTest, NoOtpField) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/UNKNOWN_TYPE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when the only OTP field is not
+// focusable.
+TEST(OtpFieldDetectorIsOtpFormTest, UnfocusableOtpField) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/ONE_TIME_CODE, /*is_focusable=*/false);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when there is a focusable OTP field on
+// the same origin as the main frame.
+TEST(OtpFieldDetectorIsOtpFormTest, FocusableOtpFieldSameOrigin) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/ONE_TIME_CODE, /*is_focusable=*/true);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when an OTP field is in a subdomain
+// iframe matching the main frame's TLD+1.
+TEST(OtpFieldDetectorIsOtpFormTest, SameTldPlusOneSubdomain) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://sub.example.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when
+// `kAutofillRestrictOtpToSameTldPlusOne` is enabled and an OTP field is in a
+// cross-origin iframe with mismatched TLD+1.
+TEST(OtpFieldDetectorIsOtpFormTest, MismatchedTldPlusOneFeatureEnabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://attacker.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when
+// `kAutofillRestrictOtpToSameTldPlusOne` is disabled even if an OTP field is in
+// a cross-origin iframe.
+TEST(OtpFieldDetectorIsOtpFormTest, MismatchedTldPlusOneFeatureDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://attacker.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when multiple OTP fields are present and
+// at least one has a mismatched TLD+1 with the main frame.
+TEST(OtpFieldDetectorIsOtpFormTest, MultipleOtpFieldsOneMismatched) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ FormData form;
+ form.set_main_frame_origin(url::Origin::Create(GURL("https://example.com")));
+ FormFieldData field1;
+ field1.set_origin(url::Origin::Create(GURL("https://example.com")));
+ field1.set_is_focusable(true);
+ FormFieldData field2;
+ field2.set_origin(url::Origin::Create(GURL("https://attacker.com")));
+ field2.set_is_focusable(true);
+ form.set_fields({field1, field2});
+
+ FormStructure form_structure(form);
+ ASSERT_EQ(form_structure.fields().size(), 2u);
+ form_structure.field(0)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt);
+ form_structure.field(1)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt);
+
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(form_structure));
+}
... (file diff truncated)
Regression Test / PoC
diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
index 35885d10..e2f9f85 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -7157,7 +7157,7 @@
// suggests that.
const std::vector<std::string> otp_values = {"123456"};
EXPECT_CALL(otp_manager(), GetOtpSuggestions)
- .WillOnce(RunOnceCallback<1>(otp_values));
+ .WillOnce(RunOnceCallback<2>(otp_values));
OnAskForValuesToFill(form, form.fields()[0]);
EXPECT_TRUE(external_delegate()->on_suggestions_returned_seen());
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
index df8b675b..6032d06 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_field_detector_unittest.cc
@@ -11,6 +11,8 @@
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
+#include "components/autofill/core/browser/autofill_type.h"
+#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_manager_test_api.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
@@ -421,4 +423,135 @@
OtpFieldDetectorAutofillManagerObserverTest,
testing::Bool());
+namespace {
+
+// Helper to construct a FormStructure with a single field for testing
+// `IsOtpForm`.
+std::unique_ptr<FormStructure> CreateFormWithField(
+ const url::Origin& main_frame_origin,
+ const url::Origin& field_origin,
+ FieldType type,
+ bool is_focusable = true) {
+ FormData form;
+ form.set_main_frame_origin(main_frame_origin);
+ FormFieldData field;
+ field.set_origin(field_origin);
+ field.set_is_focusable(is_focusable);
+ form.set_fields({field});
+
+ auto form_structure = std::make_unique<FormStructure>(form);
+ if (form_structure->fields().empty()) {
+ return nullptr;
+ }
+ form_structure->field(0)->SetTypeTo(AutofillType(type), std::nullopt);
+ return form_structure;
+}
+
+} // namespace
+
+// Tests that `IsOtpForm` returns false when there are no OTP fields in the
+// form.
+TEST(OtpFieldDetectorIsOtpFormTest, NoOtpField) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/UNKNOWN_TYPE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when the only OTP field is not
+// focusable.
+TEST(OtpFieldDetectorIsOtpFormTest, UnfocusableOtpField) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/ONE_TIME_CODE, /*is_focusable=*/false);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when there is a focusable OTP field on
+// the same origin as the main frame.
+TEST(OtpFieldDetectorIsOtpFormTest, FocusableOtpFieldSameOrigin) {
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*type=*/ONE_TIME_CODE, /*is_focusable=*/true);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when an OTP field is in a subdomain
+// iframe matching the main frame's TLD+1.
+TEST(OtpFieldDetectorIsOtpFormTest, SameTldPlusOneSubdomain) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://sub.example.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when
+// `kAutofillRestrictOtpToSameTldPlusOne` is enabled and an OTP field is in a
+// cross-origin iframe with mismatched TLD+1.
+TEST(OtpFieldDetectorIsOtpFormTest, MismatchedTldPlusOneFeatureEnabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://attacker.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns true when
+// `kAutofillRestrictOtpToSameTldPlusOne` is disabled even if an OTP field is in
+// a cross-origin iframe.
+TEST(OtpFieldDetectorIsOtpFormTest, MismatchedTldPlusOneFeatureDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ std::unique_ptr<FormStructure> form_structure = CreateFormWithField(
+ /*main_frame_origin=*/url::Origin::Create(GURL("https://example.com")),
+ /*field_origin=*/url::Origin::Create(GURL("https://attacker.com")),
+ /*type=*/ONE_TIME_CODE);
+ ASSERT_TRUE(form_structure);
+ EXPECT_TRUE(OtpFieldDetector::IsOtpForm(*form_structure));
+}
+
+// Tests that `IsOtpForm` returns false when multiple OTP fields are present and
+// at least one has a mismatched TLD+1 with the main frame.
+TEST(OtpFieldDetectorIsOtpFormTest, MultipleOtpFieldsOneMismatched) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ features::kAutofillRestrictOtpToSameTldPlusOne);
+
+ FormData form;
+ form.set_main_frame_origin(url::Origin::Create(GURL("https://example.com")));
+ FormFieldData field1;
+ field1.set_origin(url::Origin::Create(GURL("https://example.com")));
+ field1.set_is_focusable(true);
+ FormFieldData field2;
+ field2.set_origin(url::Origin::Create(GURL("https://attacker.com")));
+ field2.set_is_focusable(true);
+ form.set_fields({field1, field2});
+
+ FormStructure form_structure(form);
+ ASSERT_EQ(form_structure.fields().size(), 2u);
+ form_structure.field(0)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt);
+ form_structure.field(1)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt);
+
+ EXPECT_FALSE(OtpFieldDetector::IsOtpForm(form_structure));
+}
+
} // namespace autofill
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
index b9279d7..ffe9d18 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
@@ -6,6 +6,7 @@
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/clock.h"
@@ -17,6 +18,7 @@
#include "components/autofill/core/browser/foundations/with_test_autofill_client_driver_manager.h"
#include "components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_test_api.h"
#include "components/autofill/core/browser/test_utils/autofill_form_test_utils.h"
+#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/autofill_test_utils.h"
#include "components/autofill/core/common/form_data.h"
#include "components/one_time_tokens/core/browser/one_time_token.h"
@@ -26,11 +28,12 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
-using autofill::test::FormDescription;
-using autofill::test::GetServerTypes;
-using base::test::RunOnceCallback;
-using one_time_tokens::OneTimeTokenServiceImpl;
-using ::testing::_;
+using ::autofill::test::FormDescription;
+using ::autofill::test::GetServerTypes;
+using ::base::test::RunOnceCallback;
+using ::one_time_tokens::OneTimeTokenServiceImpl;
+using ::testing::Test;
+
namespace autofill {
namespace {
@@ -67,7 +70,7 @@
} // namespace
-class OtpManagerImplTest : public testing::Test,
+class OtpManagerImplTest : public Test,
public WithTestAutofillClientDriverManager<> {
public:
OtpManagerImplTest() : one_time_token_service_(&sms_otp_backend_, nullptr) {}
@@ -85,8 +88,9 @@
test_field_.set_origin(url::Origin::Create(GURL("https://example.test")));
}
- void AddForm(const FormDescription& form_description) {
+ const FormStructure* AddForm(const FormDescription& form_description) {
FormData form = test::GetFormData(form_description);
+ FormGlobalId form_id = form.global_id();
auto form_structure = std::make_unique<FormStructure>(form);
test_api(*form_structure).SetFieldTypes(GetServerTypes(form_description));
test_api(*form_structure).AssignSections();
@@ -97,26 +101,36 @@
// This would typically happen during parsing but is skipped if a form is
// injected via the test API.
autofill_manager().NotifyObservers(
- &TestBrowserAutofillManager::Observer::OnFieldTypesDetermined,
- form.global_id(),
+ &TestBrowserAutofillManager::Observer::OnFieldTypesDetermined, form_id,
TestBrowserAutofillManager::Observer::FieldTypeSource::kAutofillAiModel,
/*small_forms_were_parsed=*/false);
+ return autofill_manager().FindCachedFormById(form_id);
}
- void AddFormWithOtpField() {
+ const FormStructure* AddFormWithOtpField(
+ std::optional<url::Origin> field_origin = std::nullopt,
+ std::optional<url::Origin> main_frame_origin = std::nullopt,
+ bool is_focusable = true) {
FormDescription form_description = {
- .fields = {
- {.server_type = ONE_TIME_CODE, .label = u"OTP", .name = u"otp"},
- }};
- AddForm(form_description);
+ .fields =
+ {
+ {.server_type = ONE_TIME_CODE,
+ .is_focusable = is_focusable,
+ .label = u"OTP",
+ .name = u"otp",
+ .origin = field_origin},
+ },
+ .main_frame_origin = main_frame_origin,
+ };
+ return AddForm(form_description);
}
- void AddFormWithFirstNameField() {
+ const FormStructure* AddFormWithFirstNameField() {
FormDescription form_description = {
.fields = {
{.server_type = NAME_FIRST, .label = u"First name", .name = u"fn"},
}};
- AddForm(form_description);
+ return AddForm(form_description);
}
MockOtpPhishGuardDelegate& otp_phish_guard_delegate() {
@@ -169,10 +183,12 @@
.WillOnce(RunOnceCallback<2>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
- AddFormWithOtpField();
+ const FormStructure* form = AddFormWithOtpField();
+ ASSERT_TRUE(form);
base::test::TestFuture<const std::vector<std::string>> future;
- otp_manager.GetOtpSuggestions(test_field_.origin(), future.GetCallback());
+ otp_manager.GetOtpSuggestions(*form, test_field_.origin(),
+ future.GetCallback());
ASSERT_EQ(future.Get().size(), 1u);
EXPECT_EQ(future.Get()[0], otp.value());
@@ -200,10 +216,12 @@
.WillOnce(RunOnceCallback<2>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
- AddFormWithOtpField();
+ const FormStructure* form = AddFormWithOtpField();
+ ASSERT_TRUE(form);
base::test::TestFuture<const std::vector<std::string>> future;
- otp_manager.GetOtpSuggestions(test_field_.origin(), future.GetCallback());
+ otp_manager.GetOtpSuggestions(*form, test_field_.origin(),
+ future.GetCallback());
// The future should not be ready yet, as the SMS backend has not responded.
EXPECT_FALSE(future.IsReady());
@@ -231,21 +249,25 @@
.WillOnce(RunOnceCallback<2>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
- AddFormWithOtpField();
... (truncated)