CVE-2026-87565
Overview
Background
- SMS OTP Autofill
- a Chrome feature that captures one-time codes delivered by SMS and offers them as autofill suggestions for fields typed as
ONE_TIME_CODE. - Secure context (`AutofillClient::IsContextSecure`)
- a client method that reports whether the page’s committed main-frame origin is trustworthy (e.g. HTTPS), used to gate sensitive autofill behavior.
- `OtpManager::GetOtpSuggestions`
- the browser-side call that retrieves the currently available SMS one-time codes and returns them as suggestions via a callback.
- Dual suggestion pipelines
- OTP suggestions can be produced through the legacy
BrowserAutofillManagertwo-phase path or through the newerOtpSuggestionGenerator, so a gate must exist in both.
Root Cause Analysis
Before the patch, both suggestion pipelines fetched and surfaced SMS one-time codes for ONE_TIME_CODE fields without ever consulting whether the page was a secure context. The invariant that a sensitive, single-use authentication code should only be offered into a trustworthy (HTTPS) origin was therefore never enforced, so a one-time code could be suggested and autofilled on a plain http:// page.
The fix adds an IsContextSecure() guard at each entry point: in the legacy Phase 1, an insecure context now runs the generate_suggestions_and_maybe_show_ui_phase2 callback with an empty result and returns before otp_manager_->GetOtpSuggestions(...) is called; in the legacy Phase 2, BuildOtpSuggestions(one_time_passwords) is only executed when the context is secure; and in OtpSuggestionGenerator, an insecure context returns an empty {kOneTimePassword, {}} payload before the manager is queried. Because every guard executes ahead of the OTP-fetch/build sink, no OTP data reaches an insecure page. The removed #if BUILDFLAG(IS_ANDROID) string include is only dead-code cleanup, since the old code paved a warning-message approach that was dropped in favor of silent suppression.
IsContextSecure() and silently returns empty suggestions otherwise.Attack Path
- Serve an insecure OTP form
An attacker hosts (or, as a network attacker, injects into) an
http://page containing a field that Chrome classifies asONE_TIME_CODE. - Victim receives an SMS code While the victim is on that page, a legitimate SMS one-time code arrives and is captured by Chrome’s OTP mechanism.
- Suggestion is generated without a security gate
Chrome parses the field, and the unpatched
BrowserAutofillManager/OtpSuggestionGeneratorpath callsOtpManager::GetOtpSuggestionsand builds a suggestion regardless of context security. - Code is exposed on the insecure page The one-time code is presented (and potentially autofilled) into the untrusted HTTP context, disclosing it to the page and to any on-path network observer.
- Attacker obtains the OTP (inferred) The attacker-controlled page or network position reads the disclosed code and uses it to complete an authentication step.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc |
modified | |
OtpSuggestionGeneratorTestcomponents/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc |
modified | |
TEST_Fcomponents/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc |
modified |
Files Changed
components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cccomponents/autofill/core/browser/foundations/browser_autofill_manager_unittest.cccomponents/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator.cccomponents/autofill/core/browser/foundations/browser_autofill_manager.cc
Audit Directions
- Sensitive-data autofill without a secure-context gateAudit every autofill/suggestion producer that surfaces secrets (OTPs, passwords, payment data) and confirm each independent entry point checks
IsContextSecure()before fetching or building suggestions. - Parallel legacy and new pipelinesWhen a feature is migrated to a new generator, verify a security guard added to one pipeline is mirrored in the other, since
BrowserAutofillManagerandOtpSuggestionGeneratoreach needed the same fix. - Guard-before-sink orderingCheck that any newly added early-exit guard runs before the data-fetch/callback sink (as here, before
GetOtpSuggestionsandBuildOtpSuggestions), rather than after the sensitive value is already retrieved.
Patch
From a7991af9d476b4ca205454aead73a71f6a1d08e5 Mon Sep 17 00:00:00 2001 From: Matthias Koerber <[email protected]> Date: Tue, 04 Aug 2026 04:01:13 -0700 Subject: [PATCH] [OTP] Restrict SMS OTP Autofill to secure contexts This CL ensures that SMS OTP suggestions are only offered on secure (HTTPS) contexts. To achieve this, we silently suppress OTP suggestions when the context is insecure: 1. In the legacy path (BrowserAutofillManager), we skip fetching OTPs from OtpManager in Phase 1 if the context is insecure, and ensure we don't build suggestions in Phase 2. 2. In the new suggestion pipeline (OtpSuggestionGenerator), we early return with empty suggestions if the context is insecure. Silent suppression was chosen over showing a warning message (similar to Payments) to avoid UI noise on HTTP pages when no OTP is actually available. Fixed: 532968511 Change-Id: Iaec97661cb16f38c761473fc8dc462ce02b29000 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8177172 Auto-Submit: Matthias Körber <[email protected]> Commit-Queue: Matthias Körber <[email protected]> Reviewed-by: Jihad Hanna <[email protected]> Cr-Commit-Position: refs/heads/main@{#1673232} --- diff --git a/components/autofill/core/browser/foundations/browser_autofill_manager.cc b/components/autofill/core/browser/foundations/browser_autofill_manager.cc index f09f106c..57c06fc 100644 --- a/components/autofill/core/browser/foundations/browser_autofill_manager.cc +++ b/components/autofill/core/browser/foundations/browser_autofill_manager.cc @@ -1502,6 +1502,10 @@ // autocomplete. if (otp_manager_ && 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( std::move(generate_suggestions_and_maybe_show_ui_phase2)); return; @@ -3291,7 +3295,9 @@ } break; case FillingProduct::kOneTimePassword: - suggestions = BuildOtpSuggestions(one_time_passwords); + if (client().IsContextSecure()) { + suggestions = BuildOtpSuggestions(one_time_passwords); + } break; case FillingProduct::kAtMemory: return {}; 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 a006afd..8fd0e5d 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 @@ -15,9 +15,6 @@ #include "components/autofill/core/browser/data_quality/addresses/profile_token_quality.h" #include "components/autofill/core/browser/field_types.h" #include "components/autofill/core/browser/foundations/autofill_client.h" -#if BUILDFLAG(IS_ANDROID) -#include "components/strings/grit/components_strings.h" -#endif #include "components/autofill/core/browser/integrators/one_time_tokens/otp_manager.h" #include "components/autofill/core/browser/suggestions/suggestion.h" #include "components/autofill/core/browser/suggestions/suggestion_generator.h" @@ -83,6 +80,11 @@ return; } + if (!client.IsContextSecure()) { + std::move(callback).Run({SuggestionDataSource::kOneTimePassword, {}}); + return; + } + otp_manager_->GetOtpSuggestions( 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 65976418..19de7d41 100644 --- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc +++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc @@ -7125,6 +7125,44 @@ EXPECT_EQ(otp_value, filled_fields[0].value()); } +// Test that OTP suggestions are silently suppressed on insecure contexts. +TEST_F(BrowserAutofillManagerOtpSuggestionsTest, + OtpSuggestions_InsecureContext) { + autofill_client().set_last_committed_primary_main_frame_url( + GURL("http://example.com")); + + FormData form = + test::GetFormData({.fields = { + {.label = u"Enter one time code", + .form_control_type = FormControlType::kInputText}, + }}); + + // Simulate form parsing results. + auto form_structure = std::make_unique<FormStructure>(form); + form_structure->field(0)->set_heuristic_type( + HeuristicSource::kPasswordManagerMachineLearning, + FieldType::ONE_TIME_CODE); + test_api(autofill_manager()).AddSeenFormStructure(std::move(form_structure)); + + // We should NOT call GetOtpSuggestions on the manager. + EXPECT_CALL(otp_manager(), GetOtpSuggestions).Times(0); + + ON_CALL(autocomplete_history_manager(), OnGetSingleFieldSuggestions) + .WillByDefault( + [](const FormData& form, const FormStructure* form_structure, + const FormFieldData& field, const AutofillField* autofill_field, + const AutofillClient& client, + SingleFieldFillRouter::OnSuggestionsReturnedCallback + on_suggestions_returned) { + std::move(on_suggestions_returned).Run(field.global_id(), {}); + }); + + OnAskForValuesToFill(form, form.fields()[0]); + + // We expect no suggestions. + external_delegate()->CheckNoSuggestions(form.fields()[0].global_id()); +} + // Tests that FillOrPreviewForm correctly passes the blocked_fields to the // FormFiller. TEST_F(BrowserAutofillManagerTest, FillOrPreviewForm_BlockedFields) { diff --git a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc index 370a1d7c..66eaee6 100644 --- a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc +++ b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc @@ -19,16 +19,19 @@ #include "components/autofill/core/common/autofill_test_utils.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" namespace autofill { using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::Pair; class OtpSuggestionGeneratorTest : public testing::Test { protected: OtpSuggestionGeneratorTest() = default; - AutofillClient& client() { return autofill_client_; } + TestAutofillClient& client() { return autofill_client_; } OtpSuggestionGenerator& generator() { return generator_; } MockOtpManager& otp_manager() { return otp_manager_; } @@ -94,4 +97,28 @@ #endif } +// Test that OTP suggestions are silently suppressed on insecure contexts. +TEST_F(OtpSuggestionGeneratorTest, GenerateOtpSuggestions_InsecureContext) { + client().set_last_committed_primary_main_frame_url( + GURL("http://example.com")); + FormData form = test::GetFormData({.fields = {{.role = ONE_TIME_CODE}}}); + FormStructure form_structure(form); + form_structure.field(0)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt); + + EXPECT_CALL(otp_manager(), GetOtpSuggestions).Times(0); + + base::MockCallback< + base::OnceCallback<void(SuggestionGenerator::ReturnedSuggestions)>> + suggestions_generated_callback; + + EXPECT_CALL( + suggestions_generated_callback, + Run(Pair(SuggestionGenerator::SuggestionDataSource::kOneTimePassword, + IsEmpty()))); + + generator().GenerateSuggestions(form, form.fields()[0], &form_structure, + form_structure.field(0), client(), + suggestions_generated_callback.Get()); +} + } // namespace autofill
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 65976418..19de7d41 100644
--- a/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/foundations/browser_autofill_manager_unittest.cc
@@ -7125,6 +7125,44 @@
EXPECT_EQ(otp_value, filled_fields[0].value());
}
+// Test that OTP suggestions are silently suppressed on insecure contexts.
+TEST_F(BrowserAutofillManagerOtpSuggestionsTest,
+ OtpSuggestions_InsecureContext) {
+ autofill_client().set_last_committed_primary_main_frame_url(
+ GURL("http://example.com"));
+
+ FormData form =
+ test::GetFormData({.fields = {
+ {.label = u"Enter one time code",
+ .form_control_type = FormControlType::kInputText},
+ }});
+
+ // Simulate form parsing results.
+ auto form_structure = std::make_unique<FormStructure>(form);
+ form_structure->field(0)->set_heuristic_type(
+ HeuristicSource::kPasswordManagerMachineLearning,
+ FieldType::ONE_TIME_CODE);
+ test_api(autofill_manager()).AddSeenFormStructure(std::move(form_structure));
+
+ // We should NOT call GetOtpSuggestions on the manager.
+ EXPECT_CALL(otp_manager(), GetOtpSuggestions).Times(0);
+
+ ON_CALL(autocomplete_history_manager(), OnGetSingleFieldSuggestions)
+ .WillByDefault(
+ [](const FormData& form, const FormStructure* form_structure,
+ const FormFieldData& field, const AutofillField* autofill_field,
+ const AutofillClient& client,
+ SingleFieldFillRouter::OnSuggestionsReturnedCallback
+ on_suggestions_returned) {
+ std::move(on_suggestions_returned).Run(field.global_id(), {});
+ });
+
+ OnAskForValuesToFill(form, form.fields()[0]);
+
+ // We expect no suggestions.
+ external_delegate()->CheckNoSuggestions(form.fields()[0].global_id());
+}
+
// Tests that FillOrPreviewForm correctly passes the blocked_fields to the
// FormFiller.
TEST_F(BrowserAutofillManagerTest, FillOrPreviewForm_BlockedFields) {
diff --git a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc
index 370a1d7c..66eaee6 100644
--- a/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc
+++ b/components/autofill/core/browser/suggestions/one_time_passwords/otp_suggestion_generator_unittest.cc
@@ -19,16 +19,19 @@
#include "components/autofill/core/common/autofill_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
namespace autofill {
using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Pair;
class OtpSuggestionGeneratorTest : public testing::Test {
protected:
OtpSuggestionGeneratorTest() = default;
- AutofillClient& client() { return autofill_client_; }
+ TestAutofillClient& client() { return autofill_client_; }
OtpSuggestionGenerator& generator() { return generator_; }
MockOtpManager& otp_manager() { return otp_manager_; }
@@ -94,4 +97,28 @@
#endif
}
+// Test that OTP suggestions are silently suppressed on insecure contexts.
+TEST_F(OtpSuggestionGeneratorTest, GenerateOtpSuggestions_InsecureContext) {
+ client().set_last_committed_primary_main_frame_url(
+ GURL("http://example.com"));
+ FormData form = test::GetFormData({.fields = {{.role = ONE_TIME_CODE}}});
+ FormStructure form_structure(form);
+ form_structure.field(0)->SetTypeTo(AutofillType(ONE_TIME_CODE), std::nullopt);
+
+ EXPECT_CALL(otp_manager(), GetOtpSuggestions).Times(0);
+
+ base::MockCallback<
+ base::OnceCallback<void(SuggestionGenerator::ReturnedSuggestions)>>
+ suggestions_generated_callback;
+
+ EXPECT_CALL(
+ suggestions_generated_callback,
+ Run(Pair(SuggestionGenerator::SuggestionDataSource::kOneTimePassword,
+ IsEmpty())));
+
+ generator().GenerateSuggestions(form, form.fields()[0], &form_structure,
+ form_structure.field(0), client(),
+ suggestions_generated_callback.Get());
+}
+
} // namespace autofill