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 <jochen@chromium.org>
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é <battre@chromium.org>
Commit-Queue: Jochen Eisinger <jochen@chromium.org>
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 (battre@chromium.org)
(not sure whether we credit security bugs that we find ourselves…)