CVE-2026-13949
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc |
modified |
Files Changed
chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.ccchrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
Patch
From 2f0a14faf2f34b9fc27154d3cd481371e1e20925 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
Date: Wed, 20 May 2026 02:02:54 -0700
Subject: [PATCH] [KeyboardAccessory] Fix mandatory re-auth bypass for local IBANs
A logic flaw in the Android keyboard accessory's payment method
controller allowed locally-stored IBANs to bypass Mandatory Re-
authentication. This occurred because local IBAN chips were initialized
with an empty identifier, triggering a direct-fill path that avoided
the IbanAccessManager.
This CL fixes the issue by:
1. Assigning the IBAN's GUID as the identifier for local IBANs in
TranslateIban().
2. Updating FetchIfIban() to handle local IBANs and route them through
IbanAccessManager::FetchValue(), where mandatory re-authentication is
correctly enforced.
Fixed: b:513311569
Test: Unit tests passed via UTR (android-13-x64-rel builder).
Change-Id: Iee209f9ddae210c0f9f68d27f740377912662d62
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849357
Reviewed-by: Qihui Zhao <qihuizhao@google.com>
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1633436}
---
diff --git a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
index d7073ef8..4a136a9 100644
--- a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
+++ b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
@@ -184,10 +184,8 @@
IbanInfo TranslateIban(const Iban& data) {
bool is_local = data.record_type() == Iban::kLocalIban;
- std::string id_string;
- if (!is_local) {
- id_string = base::NumberToString(data.instrument_id());
- }
+ std::string id_string =
+ is_local ? data.guid() : base::NumberToString(data.instrument_id());
IbanInfo iban_info(data.GetIdentifierStringForAutofillDisplay(),
is_local ? data.value() : std::u16string(), id_string);
@@ -616,6 +614,9 @@
std::vector<Iban> ibans = GetIbans();
auto iban_iter =
std::ranges::find_if(ibans, [&selection_id](const Iban& available_iban) {
+ if (available_iban.record_type() == Iban::kLocalIban) {
+ return available_iban.guid() == selection_id;
+ }
return available_iban.record_type() == Iban::kServerIban &&
base::NumberToString(available_iban.instrument_id()) ==
selection_id;
@@ -625,13 +626,19 @@
return false;
}
- Suggestion::InstrumentId instrument_id(iban_iter->instrument_id());
+ Suggestion::Payload payload;
+ if (iban_iter->record_type() == Iban::kLocalIban) {
+ payload = Suggestion::Guid(iban_iter->guid());
+ } else {
+ payload = Suggestion::InstrumentId(iban_iter->instrument_id());
+ }
+
GetAutofillManager()
->client()
.GetPaymentsAutofillClient()
->GetIbanAccessManager()
->FetchValue(
- instrument_id,
+ payload,
base::BindOnce(&PaymentMethodAccessoryControllerImpl::ApplyToField,
weak_ptr_factory_.GetWeakPtr()));
return true;
diff --git a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
index d8741864..6801083a 100644
--- a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
@@ -603,7 +603,7 @@
Iban iban;
iban.set_value(std::u16string(test::kIbanValue16));
- paydm().AddAsLocalIban(iban);
+ std::string guid = paydm().AddAsLocalIban(iban);
EXPECT_CALL(filling_source_observer_,
Run(controller(), IsFillingSourceAvailable(true)));
@@ -634,7 +634,7 @@
.AppendSimpleField(AccessorySuggestionType::kCreditCardCvc,
std::u16string())
.AddIbanInfo(iban.GetIdentifierStringForAutofillDisplay(),
- iban.value(), /*id=*/"")
+ iban.value(), /*id=*/guid)
.Build());
}
@@ -652,6 +652,7 @@
.SetSuggestionType(AccessorySuggestionType::kIban)
.SetDisplayText(iban.GetIdentifierStringForAutofillDisplay())
.SetTextToFill(iban.value())
+ .SetId(guid)
.SetSelectable(true)
.Build();
@@ -660,6 +661,12 @@
FieldGlobalId field_id{.frame_token = LocalFrameToken(*rfh->GetFrameToken()),
.renderer_id = FieldRendererId(123)};
+ EXPECT_CALL(iban_access_manager(), FetchValue(_, _))
+ .WillOnce([&iban](const Suggestion::Payload& payload,
+ IbanAccessManager::OnIbanFetchedCallback callback) {
+ std::move(callback).Run(iban.value());
+ });
+
EXPECT_CALL(autofill_driver(),
ApplyFieldAction(mojom::FieldActionType::kReplaceAll,
mojom::ActionPersistence::kFill, field_id,
Regression Test / PoC
diff --git a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
index d8741864..6801083a 100644
--- a/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl_unittest.cc
@@ -603,7 +603,7 @@
Iban iban;
iban.set_value(std::u16string(test::kIbanValue16));
- paydm().AddAsLocalIban(iban);
+ std::string guid = paydm().AddAsLocalIban(iban);
EXPECT_CALL(filling_source_observer_,
Run(controller(), IsFillingSourceAvailable(true)));
@@ -634,7 +634,7 @@
.AppendSimpleField(AccessorySuggestionType::kCreditCardCvc,
std::u16string())
.AddIbanInfo(iban.GetIdentifierStringForAutofillDisplay(),
- iban.value(), /*id=*/"")
+ iban.value(), /*id=*/guid)
.Build());
}
@@ -652,6 +652,7 @@
.SetSuggestionType(AccessorySuggestionType::kIban)
.SetDisplayText(iban.GetIdentifierStringForAutofillDisplay())
.SetTextToFill(iban.value())
+ .SetId(guid)
.SetSelectable(true)
.Build();
@@ -660,6 +661,12 @@
FieldGlobalId field_id{.frame_token = LocalFrameToken(*rfh->GetFrameToken()),
.renderer_id = FieldRendererId(123)};
+ EXPECT_CALL(iban_access_manager(), FetchValue(_, _))
+ .WillOnce([&iban](const Suggestion::Payload& payload,
+ IbanAccessManager::OnIbanFetchedCallback callback) {
+ std::move(callback).Run(iban.value());
+ });
+
EXPECT_CALL(autofill_driver(),
ApplyFieldAction(mojom::FieldActionType::kReplaceAll,
mojom::ActionPersistence::kFill, field_id,
Original Bug Report
Potential Mandatory Re-auth bypass for local IBANs in Android keyboard accessory
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic flaw in the Android keyboard accessory’s payment method controller potentially allows locally-stored IBANs to bypass Mandatory Re-authentication. When a local IBAN is selected from the keyboard accessory, the controller may skip the security enforcement layer, filling the plaintext value without a biometric or PIN prompt. This occurs because local IBAN chips are initialized with an empty identifier, triggering a direct-fill path that avoids the IbanAccessManager.
Affected files:
chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc
Estimated timestamp from git blame: 2024-05-07
Summary
A potential vulnerability in PaymentMethodAccessoryControllerImpl on Android allows local IBANs to bypass the ‘Mandatory Re-auth’ security feature. While Chrome correctly enforces biometric or screen-lock verification for credit cards and server-stored IBANs, local IBANs suggested via the keyboard accessory may follow a direct-fill path that skips all authentication checks.
Root Cause Analysis
In chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc, the TranslateIban() function builds the data structure for the keyboard-accessory chip. For local IBANs (kLocalIban), the identifier string is left empty, but the text_to_fill field is populated with the full plaintext IBAN:
// chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc:185
IbanInfo TranslateIban(const Iban& data) {
bool is_local = data.record_type() == Iban::kLocalIban;
std::string id_string;
if (!is_local) {
id_string = base::NumberToString(data.instrument_id());
}
IbanInfo iban_info(data.GetIdentifierStringForAutofillDisplay(),
is_local ? data.value() : std::u16string(), id_string);
return iban_info;
}
When a user selects the IBAN chip, OnFillingTriggered() is invoked. Because selection.id().empty() is true for local IBANs, the controller executes a ‘fast-path’ branch that calls the driver directly and returns immediately:
// chrome/browser/keyboard_accessory/android/payment_method_accessory_controller_impl.cc:294
if (selection.id().empty()) {
GetDriver()->ApplyFieldAction(mojom::FieldActionType::kReplaceAll,
mojom::ActionPersistence::kFill,
focused_field_id, selection.text_to_fill());
return;
}
This bypasses FetchIfIban() and the subsequent call to IbanAccessManager::FetchValue(). FetchValue() is the primary enforcement point that checks IsMandatoryReauthEnabled() and triggers the MandatoryReauthManager to prompt the user for authentication. By circumventing this manager, the plaintext IBAN is filled into the webpage without any security challenge.
Potential Impact
An attacker-controlled website could potentially harvest a victim’s full bank account number (IBAN) from Chrome’s local storage. If a user has enabled ‘Mandatory Re-auth’ for payment methods, they have a reasonable expectation that their IBANs are protected by biometrics/PIN. This flaw breaks that security invariant, as the UI displays only a masked identifier, providing no indication to the user that the full value was released without authentication.
Suggested Reproduction Steps (Potential)
- On Chrome for Android, save a local IBAN in Settings > Payment methods.
- Enable ‘Verify it’s you when using autofill’ in the Payment methods settings.
- Navigate to a website with an IBAN input field (e.g.,
<input autocomplete="iban">). - Focus the input and open the Payments tab in the keyboard accessory.
- Select the IBAN chip.
- Observe if the plaintext IBAN is filled without a biometric or PIN prompt.
Recommended Fix
Modify TranslateIban() to ensure all IBANs (including local ones) are assigned a unique identifier (such as the IBAN’s GUID). This will force OnFillingTriggered() to route the request through FetchIfIban() and IbanAccessManager::FetchValue(), where mandatory re-authentication is correctly enforced.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.