CVE-2026-17730
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/autofill/content/renderer/autofill_agent.cc |
modified | |
AutofillAgentBruteForceProbingTestcomponents/autofill/content/renderer/autofill_agent_browsertest.cc |
modified | |
ifcomponents/autofill/content/renderer/autofill_agent_browsertest.cc |
modified |
Files Changed
components/autofill/content/renderer/autofill_agent.cccomponents/autofill/content/renderer/autofill_agent.hcomponents/autofill/content/renderer/autofill_agent_browsertest.cc
Patch
From aca0cfec065c928b60c8f974343e5abec77ecd56 Mon Sep 17 00:00:00 2001 From: Jochen Eisinger <[email protected]> Date: Mon, 15 Jun 2026 16:30:38 -0700 Subject: [PATCH] [Autofill] Throttle brute-force probing of autofill data Malicious web pages can attempt to steal saved autofill data via a side-channel brute-force attack by rapidly cycling input prefixes and monitoring :autofill state changes. This change mitigates probing attacks by introducing frame-scoped token bucket rate limiting on AskForValuesToFill() requests inside AutofillAgent. Legitimate interactive bursts remain unthrottled while prolonged automated probing is clamped to the replenishment rate. TAG=agy Fixed: 40057032 Change-Id: Ief9cee0e54f71bcade4be419669781ad871ef04e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7931048 Reviewed-by: Dominic Battré <[email protected]> Commit-Queue: Jochen Eisinger <[email protected]> Cr-Commit-Position: refs/heads/main@{#1647165} --- diff --git a/components/autofill/content/renderer/autofill_agent.cc b/components/autofill/content/renderer/autofill_agent.cc index 34afa0f..89b9801 100644 --- a/components/autofill/content/renderer/autofill_agent.cc +++ b/components/autofill/content/renderer/autofill_agent.cc @@ -539,6 +539,7 @@ form_tracker_->SetUserGestureRequired(config_.user_gesture_required); registry->AddInterface<mojom::AutofillAgent>(base::BindRepeating( &AutofillAgent::BindPendingReceiver, base::Unretained(this))); + ResetTokenBucket(); } // The destructor is not guaranteed to be called. Destruction happens (only) @@ -583,6 +584,7 @@ input_warnings_.has_warned = false; input_warnings_.remove_listeners.clear(); email_verification_observer_.Reset(); + ResetTokenBucket(); } void AutofillAgent::DidDispatchDOMContentLoadedEvent() { @@ -1573,7 +1575,18 @@ password_generation_agent_->PreviewGenerationSuggestion(password); } +void AutofillAgent::ResetTokenBucket() { + ask_for_values_to_fill_throttle_.tokens = + features::kAutofillThrottleBruteForceProbingMaxTokens.Get(); + ask_for_values_to_fill_throttle_.last_replenish_time = base::TimeTicks::Now(); +} + bool AutofillAgent::ShouldThrottleAskForValuesToFill(FieldRendererId field) { + // 1. Apply 100ms *per field* throttle to AskForValuesToFill. + // At least on Android, multiple AskForValuesToFill() events may be fired in + // short succession. Since getting the event handling right in AutofillAgent + // is difficult we ignore duplicate AskForValuesToFill() as a workaround. + // See crbug.com/40284788 for details. static constexpr base::TimeDelta kThrottle = base::Milliseconds(100); base::TimeTicks now = base::TimeTicks::Now(); if (field == last_ask_for_values_to_fill_.field && @@ -1581,6 +1594,43 @@ return true; } last_ask_for_values_to_fill_ = {now, field}; + + // 2. Apply a *per frame* throttle to AskForValuesToFill. + // This exists because malicious web pages can attempt to steal saved + // autofill data via a side-channel brute-force attack by rapidly cycling + // input prefixes and monitoring :autofill state changes. + if (base::FeatureList::IsEnabled( + features::kAutofillThrottleBruteForceProbing)) { + base::TimeDelta replenish_rate = + features::kAutofillThrottleBruteForceProbingReplenishRate.Get(); + const int max_tokens = + features::kAutofillThrottleBruteForceProbingMaxTokens.Get(); + + if (replenish_rate.is_positive()) { + int64_t earned_tokens = + (now - ask_for_values_to_fill_throttle_.last_replenish_time) + .IntDiv(replenish_rate); + if (earned_tokens > 0) { + if (earned_tokens >= max_tokens || + ask_for_values_to_fill_throttle_.tokens + earned_tokens >= + max_tokens) { + ask_for_values_to_fill_throttle_.tokens = max_tokens; + ask_for_values_to_fill_throttle_.last_replenish_time = now; + } else { + ask_for_values_to_fill_throttle_.tokens += + static_cast<int>(earned_tokens); + ask_for_values_to_fill_throttle_.last_replenish_time += + earned_tokens * replenish_rate; + } + } + } + + if (ask_for_values_to_fill_throttle_.tokens <= 0) { + return true; // Throttled due to burst budget exhaustion. + } + ask_for_values_to_fill_throttle_.tokens--; + } + return false; } diff --git a/components/autofill/content/renderer/autofill_agent.h b/components/autofill/content/renderer/autofill_agent.h index 335642f..8dfee4f7 100644 --- a/components/autofill/content/renderer/autofill_agent.h +++ b/components/autofill/content/renderer/autofill_agent.h @@ -403,11 +403,11 @@ // updating while the scroll signal is dispatched. void DidChangeScrollOffsetImpl(); - // At least on Android, multiple AskForValuesToFill() events may be fired in - // short succession. Since getting the event handling right in AutofillAgent - // is difficult we ignore duplicate AskForValuesToFill() as a workaround. - // See crbug.com/40284788 for details. + // Returns if a call to `AskForValuesToFill()` should be skipped. + // Rate limits exist per field and per frame. See the function + // body for further details. bool ShouldThrottleAskForValuesToFill(FieldRendererId field); + void ResetTokenBucket(); // Shows Password Manager, password generation, or Autofill suggestions for // `element`. This call is asynchronous and may or may not lead to the showing @@ -597,6 +597,14 @@ } last_ask_for_values_to_fill_; struct { + // Remaining tokens. Calls to AskForValuesToFill() are only permitted + // while tokens remain. Each call consumes a token. Tokens are replenished + // at a capped rate. + int tokens = 0; + base::TimeTicks last_replenish_time; + } ask_for_values_to_fill_throttle_; + + struct { bool has_warned = false; std::vector<base::ScopedClosureRunner> remove_listeners; } input_warnings_; diff --git a/components/autofill/content/renderer/autofill_agent_browsertest.cc b/components/autofill/content/renderer/autofill_agent_browsertest.cc index 515b29d2..1d607e4 100644 --- a/components/autofill/content/renderer/autofill_agent_browsertest.cc +++ b/components/autofill/content/renderer/autofill_agent_browsertest.cc @@ -18,6 +18,7 @@ #include "base/containers/to_vector.h" #include "base/feature_list.h" #include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/task/current_thread.h" #include "base/test/gmock_callback_support.h" @@ -81,9 +82,11 @@ using ::testing::ElementsAreArray; using ::testing::Eq; using ::testing::Field; +using ::testing::InSequence; using ::testing::IsEmpty; using ::testing::IsNull; using ::testing::Matcher; +using ::testing::MockFunction; using ::testing::Ne; using ::testing::NiceMock; using ::testing::Optional; @@ -2426,6 +2429,170 @@ EXPECT_EQ(u"", verification_element.Value().Utf16()); } +// Malicious web pages can attempt to steal saved autofill data via a +// side-channel brute-force attack by rapidly cycling input prefixes and +// monitoring :autofill state changes. +// These tests ensure integrity of the threshold mechanisms. +class AutofillAgentBruteForceProbingTest : public AutofillAgentTest { + public: + void Init(bool enabled, + int max_tokens = 15, + base::TimeDelta replenish_rate = base::Milliseconds(750)) { + if (enabled) { + feature_list_.InitAndEnableFeatureWithParameters( + features::kAutofillThrottleBruteForceProbing, + {{features::kAutofillThrottleBruteForceProbingMaxTokens.name, + base::NumberToString(max_tokens)}, + {features::kAutofillThrottleBruteForceProbingReplenishRate.name, + base::NumberToString(replenish_rate.InMilliseconds()) + "ms"}}); + } else { + feature_list_.InitAndDisableFeature( + features::kAutofillThrottleBruteForceProbing); + } + } + + void SetupHtmlAndGetElements(blink::WebFormControlElement& f1, + blink::WebFormControlElement& f2) { + EXPECT_CALL(autofill_driver(), FormsSeen); + LoadHTML(R"( + <form> + <input id=f1> + <input id=f2> + </form>
Regression Test / PoC
diff --git a/components/autofill/content/renderer/autofill_agent_browsertest.cc b/components/autofill/content/renderer/autofill_agent_browsertest.cc
index 515b29d2..1d607e4 100644
--- a/components/autofill/content/renderer/autofill_agent_browsertest.cc
+++ b/components/autofill/content/renderer/autofill_agent_browsertest.cc
@@ -18,6 +18,7 @@
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/run_loop.h"
+#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/current_thread.h"
#include "base/test/gmock_callback_support.h"
@@ -81,9 +82,11 @@
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Field;
+using ::testing::InSequence;
using ::testing::IsEmpty;
using ::testing::IsNull;
using ::testing::Matcher;
+using ::testing::MockFunction;
using ::testing::Ne;
using ::testing::NiceMock;
using ::testing::Optional;
@@ -2426,6 +2429,170 @@
EXPECT_EQ(u"", verification_element.Value().Utf16());
}
+// Malicious web pages can attempt to steal saved autofill data via a
+// side-channel brute-force attack by rapidly cycling input prefixes and
+// monitoring :autofill state changes.
+// These tests ensure integrity of the threshold mechanisms.
+class AutofillAgentBruteForceProbingTest : public AutofillAgentTest {
+ public:
+ void Init(bool enabled,
+ int max_tokens = 15,
+ base::TimeDelta replenish_rate = base::Milliseconds(750)) {
+ if (enabled) {
+ feature_list_.InitAndEnableFeatureWithParameters(
+ features::kAutofillThrottleBruteForceProbing,
+ {{features::kAutofillThrottleBruteForceProbingMaxTokens.name,
+ base::NumberToString(max_tokens)},
+ {features::kAutofillThrottleBruteForceProbingReplenishRate.name,
+ base::NumberToString(replenish_rate.InMilliseconds()) + "ms"}});
+ } else {
+ feature_list_.InitAndDisableFeature(
+ features::kAutofillThrottleBruteForceProbing);
+ }
+ }
+
+ void SetupHtmlAndGetElements(blink::WebFormControlElement& f1,
+ blink::WebFormControlElement& f2) {
+ EXPECT_CALL(autofill_driver(), FormsSeen);
+ LoadHTML(R"(
+ <form>
+ <input id=f1>
+ <input id=f2>
+ </form>
+ )");
+ WaitForFormsSeen();
+ f1 = GetFormControlElementById("f1");
+ f2 = GetFormControlElementById("f2");
+ }
+
+ void ShowSuggestion(const blink::WebFormControlElement& element) {
+ test_api(autofill_agent())
+ .ShowSuggestions(
+ element,
+ AutofillSuggestionTriggerSource::kFormControlElementClicked,
+ /*form_cache=*/{},
+ /*password_request=*/std::nullopt);
+ }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(AutofillAgentBruteForceProbingTest, NormalUsageIsNotThrottled) {
+ Init(/*enabled=*/true, /*max_tokens=*/5,
+ /*replenish_rate=*/base::Milliseconds(500));
+ blink::WebFormControlElement f1;
+ blink::WebFormControlElement f2;
+ SetupHtmlAndGetElements(f1, f2);
+
+ // 3 calls (under the 5 token limit) should be permitted.
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(3);
+ for (int i = 0; i < 3; ++i) {
+ ShowSuggestion(i % 2 == 0 ? f1 : f2);
+ }
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+}
+
+// Verify that `ShowSuggestion` calls do not trigger lookups for
+// data once a burst exceeds the number of permitted calls.
+TEST_F(AutofillAgentBruteForceProbingTest, BurstExceedsMaxTokens) {
+ Init(/*enabled=*/true, /*max_tokens=*/3,
+ /*replenish_rate=*/base::Milliseconds(500));
+ blink::WebFormControlElement f1;
+ blink::WebFormControlElement f2;
+ SetupHtmlAndGetElements(f1, f2);
+
+ MockFunction<void(std::string_view)> check;
+ {
+ InSequence s;
+ // Phase 1: 3 calls permitted by burst budget.
+ EXPECT_CALL(check, Call("Phase 1"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(3);
+ // Phase 2: 4th call should be throttled.
+ EXPECT_CALL(check, Call("Phase 2"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
+ }
+
+ // Phase 1: 3 calls permitted by burst budget.
+ check.Call("Phase 1");
+ for (int i = 0; i < 3; ++i) {
+ ShowSuggestion(i % 2 == 0 ? f1 : f2);
+ }
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+
+ // Phase 2: 4th call should be throttled.
+ check.Call("Phase 2");
+ ShowSuggestion(f2);
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+}
+
+TEST_F(AutofillAgentBruteForceProbingTest, TokenReplenishing) {
+ Init(/*enabled=*/true, /*max_tokens=*/2,
+ /*replenish_rate=*/base::Milliseconds(500));
+ blink::WebFormControlElement f1;
+ blink::WebFormControlElement f2;
+ SetupHtmlAndGetElements(f1, f2);
+
+ MockFunction<void(std::string_view)> check;
+ {
+ InSequence s;
+ // Exhaust tokens (2 calls).
+ EXPECT_CALL(check, Call("Phase 1"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(2);
+
+ // Verify currently empty bucket throttles.
+ EXPECT_CALL(check, Call("Phase 2"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
+
+ // Exactly 1 new call should be permitted.
+ EXPECT_CALL(check, Call("Phase 3"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(1);
+
+ // Next call immediately after should be throttled again.
+ EXPECT_CALL(check, Call("Phase 4"));
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
+ }
+
+ // Exhaust tokens (2 calls).
+ check.Call("Phase 1");
+ ShowSuggestion(f1);
+ ShowSuggestion(f2);
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+
+ // Verify currently empty bucket throttles.
+ check.Call("Phase 2");
+ ShowSuggestion(f1);
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+
+ // Advance time by 500ms to earn exactly 1 token.
+ task_environment_.FastForwardBy(base::Milliseconds(500));
+
+ // Exactly 1 new call should be permitted.
+ check.Call("Phase 3");
+ ShowSuggestion(f2);
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+
+ // Next call immediately after should be throttled again.
+ check.Call("Phase 4");
+ ShowSuggestion(f1);
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+}
+
+TEST_F(AutofillAgentBruteForceProbingTest, FeatureDisabled) {
+ Init(/*enabled=*/false, /*max_tokens=*/2,
+ /*replenish_rate=*/base::Milliseconds(500));
+ blink::WebFormControlElement f1;
+ blink::WebFormControlElement f2;
+ SetupHtmlAndGetElements(f1, f2);
+
+ // When disabled, calls beyond max_tokens (2) should be permitted.
+ EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(4);
+ for (int i = 0; i < 4; ++i) {
+ ShowSuggestion(i % 2 == 0 ? f1 : f2);
+ }
+ task_environment_.FastForwardBy(base::Milliseconds(0));
+}
+
} // namespace
} // namespace autofill
Original Bug Report
Security: Steal autofill data of one field via :-webkit-autofill Pseudo Selector
VULNERABILITY DETAILS
It is possible to steal one autofill value (at a time) via the :-webkit-autofill Pseudo Selector if you can convince the user to keep pressing the cursor down.
VERSION
Chrome Version: 92.0.4515.159 + stable and probably all others
Operating System: any desktop OS
REPRODUCTION CASE
Source code: https://codebin.googleplex.com/#/g9f6c9mum6c
You can try it at: https://codebin.googleplex.com/view/g9f6c9mum6c
The code contains the description of how this works. Basically we exploit that we can check whether an input element is in preview state via :-webkit-autofill or via :-internal-autofill-previewed and brute force possible prefixes while we ask the user to press the cursor down key.
CREDIT INFORMATION
Reporter credit: Dominic Battre ([email protected])
(not sure whether we credit security bugs that we find ourselves…)