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 <[email protected]> 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 <[email protected]> Commit-Queue: Matthias Körber <[email protected]> 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)