CVE-2026-17838
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mm |
modified | |
ifios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm |
modified |
Files Changed
ios/chrome/browser/autofill/form_input_accessory/coordinator/BUILD.gnios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_coordinator.mmios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.hios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mmios/chrome/browser/autofill/form_input_accessory/ui/BUILD.gnios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller.mmios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller_delegate.hios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.hios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm
Patch
From bd26c5cb1c3700eed7e43203f87d08d836fd730a Mon Sep 17 00:00:00 2001 From: Sourav <[email protected]> Date: Thu, 02 Jul 2026 11:22:13 -0700 Subject: [PATCH] [iOS] Display RP ID in Keyboard Accessory suggestions This CL updates the Keyboard Accessory suggestions layout for passkeys to match the required 2-line visual design, showing the passkey username on the first line, and a RP ID mismatch warning on the second line. Screenshot: https://screenshot.googleplex.com/VpdzAKghY3UnD8B Bug: 518075952 Change-Id: I14e40caa456a37a2c9da8adf97f5ee587899844d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7975305 Reviewed-by: Alexis Hétu <[email protected]> Commit-Queue: Sourav Uttam Sinha <[email protected]> Cr-Commit-Position: refs/heads/main@{#1656184} --- diff --git a/ios/chrome/browser/autofill/form_input_accessory/coordinator/BUILD.gn b/ios/chrome/browser/autofill/form_input_accessory/coordinator/BUILD.gn index 1a59852e..3014f10b 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/coordinator/BUILD.gn +++ b/ios/chrome/browser/autofill/form_input_accessory/coordinator/BUILD.gn @@ -65,6 +65,7 @@ "//ios/chrome/browser/shared/public/features", "//ios/chrome/browser/shared/ui/util", "//ios/chrome/browser/shared/ui/util:util_swift", + "//ios/chrome/common/credential_provider:net_util", "//ios/chrome/common/ui/colors", "//ios/chrome/common/ui/elements:form_input_accessory", "//ios/chrome/common/ui/reauthentication", diff --git a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_coordinator.mm b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_coordinator.mm index f814e409..28f6d69 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_coordinator.mm +++ b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_coordinator.mm @@ -473,6 +473,12 @@ return NO; } +- (BOOL)formInputAccessoryViewController: + (FormInputAccessoryViewController*)formInputAccessoryViewController + shouldShowRPId:(NSString*)rpId { + return [_formInputAccessoryMediator shouldShowRPId:rpId]; +} + #pragma mark - AutofillSuggestionContextMenuHandler - (void)openSettingsForSuggestion:(FormSuggestion*)suggestion { diff --git a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.h b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.h index 95dabcc..868a7c4a 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.h +++ b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.h @@ -81,6 +81,10 @@ // Clears suggestions and requests suggestions again for the same form. - (void)resetSuggestions; +// Returns YES if the suggestion label with the given RP ID should show its RP +// ID. +- (BOOL)shouldShowRPId:(NSString*)rpId; + @end // Methods to allow injection in tests. diff --git a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mm b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mm index 7a7f0acf..c26bd04 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mm +++ b/ios/chrome/browser/autofill/form_input_accessory/coordinator/form_input_accessory_mediator.mm @@ -49,6 +49,7 @@ #import "ios/chrome/browser/shared/public/commands/security_alert_commands.h" #import "ios/chrome/browser/shared/public/features/features.h" #import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h" +#import "ios/chrome/common/credential_provider/net_util.h" #import "ios/chrome/common/ui/elements/form_input_accessory_view.h" #import "ios/chrome/common/ui/reauthentication/reauthentication_event.h" #import "ios/chrome/common/ui/reauthentication/reauthentication_module.h" @@ -935,6 +936,16 @@ [self didSelectSuggestion:formSuggestion atIndex:index completion:completion]; } +- (BOOL)shouldShowRPId:(NSString*)rpId { + if (!_webState) { + return NO; + } + NSString* pageHost = + base::SysUTF8ToNSString(_webState->GetLastCommittedURL().host()); + return rpId.length && pageHost.length && + !credential_provider::SecureHostsMatch(pageHost, rpId); +} + #pragma mark - PasswordCounterObserver - (void)passwordCounterChanged:(size_t)totalPasswords { diff --git a/ios/chrome/browser/autofill/form_input_accessory/ui/BUILD.gn b/ios/chrome/browser/autofill/form_input_accessory/ui/BUILD.gn index d0116c257..4ac74ba 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/ui/BUILD.gn +++ b/ios/chrome/browser/autofill/form_input_accessory/ui/BUILD.gn @@ -19,6 +19,7 @@ "//components/autofill/core/common", "//components/autofill/ios/browser", "//components/password_manager/ios", + "//components/strings", "//components/webauthn/ios:features", "//ios/chrome/app/strings", "//ios/chrome/browser/autofill/form_input_accessory/public", diff --git a/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller.mm b/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller.mm index 11b28ade..15c323f 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller.mm +++ b/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller.mm @@ -716,4 +716,11 @@ [self.contextMenuHandler openEditForSuggestion:suggestion]; } +- (BOOL)formSuggestionView:(FormSuggestionView*)formSuggestionView + shouldShowRPId:(NSString*)rpId { + return [self.formInputAccessoryViewControllerDelegate + formInputAccessoryViewController:self + shouldShowRPId:rpId]; +} + @end diff --git a/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller_delegate.h b/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller_delegate.h index 722e035..479fd36 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller_delegate.h +++ b/ios/chrome/browser/autofill/form_input_accessory/ui/form_input_accessory_view_controller_delegate.h @@ -38,6 +38,12 @@ (FormInputAccessoryViewController*)formInputAccessoryViewController isSuggestionAutofillAsync:(FormSuggestion*)formSuggestion; +// Returns YES if the suggestion label with the given RP ID should show its RP +// ID. +- (BOOL)formInputAccessoryViewController: + (FormInputAccessoryViewController*)formInputAccessoryViewController + shouldShowRPId:(NSString*)rpId; + @end #endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_UI_FORM_INPUT_ACCESSORY_VIEW_CONTROLLER_DELEGATE_H_ diff --git a/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.h b/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.h index baf245b..57a6264f 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.h +++ b/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.h @@ -20,6 +20,9 @@ // User tapped on the suggestion. - (void)didTapFormSuggestionLabel:(FormSuggestionLabel*)formSuggestionLabel; +// Request if the suggestion label with the given RP ID should show its RP ID. +- (BOOL)shouldShowRPId:(NSString*)rpId; + @end // Class for Autofill suggestion in the customized keyboard. diff --git a/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm b/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm index 2591d1a3..110f453 100644 --- a/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm +++ b/ios/chrome/browser/autofill/form_input_accessory/ui/form_suggestion_label.mm @@ -17,6 +17,7 @@ #import "components/autofill/core/common/autofill_features.h" #import "components/autofill/ios/browser/form_suggestion.h" #import "components/password_manager/ios/shared_password_controller.h" +#import "components/strings/grit/components_strings.h" #import "components/webauthn/ios/features.h" #import "ios/chrome/browser/autofill/model/features.h" #import "ios/chrome/browser/autofill/model/form_suggestion_constants.h" @@ -393,6 +394,21 @@ [stackView addArrangedSubview:text_label]; } +// Returns the display description for a suggestion. +NSString* DisplayDescriptionForSuggestion(FormSuggestion* suggestion, + BOOL showRPId) { + if (suggestion.type == autofill::SuggestionType::kWebauthnCredential) { + NSString* passkeyLabel = + l10n_util::GetNSString(IDS_IOS_PASSKEY_SUGGESTION_LABEL); + if (showRPId) { + return [NSString + stringWithFormat:@"%@ • %@", passkeyLabel, suggestion.minorValue]; + } + return passkeyLabel; + } + return suggestion.displayDescription; +} + } // namespace @interface FormSuggestionLabel () <UIContextMenuInteractionDelegate> @@ -476,12 +492,26 @@ ? PasswordSuggestionDisplayText(suggestion.value) : suggestion.value; + BOOL isPasskey = + suggestion.type == autofill::SuggestionType::kWebauthnCredential; + + if (isPasskey && [suggestionText length] == 0) { + suggestionText = + l10n_util::GetNSString(IDS_IOS_CREDENTIAL_BOTTOM_SHEET_NO_USERNAME); + } + + NSString* displayDescription = DisplayDescriptionForSuggestion( + suggestion, + isPasskey && [delegate shouldShowRPId:suggestion.minorValue]); +
Original Bug Report
Potential GPM passkey suggestion bottom sheet origin spoofing on iOS
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: An origin-spoofing issue exists in the Google Password Manager (GPM) passkey assertion bottom sheet on iOS. When a cross-origin iframe requests a passkey assertion, the modal bottom sheet displays the top-level frame’s origin instead of the requesting iframe’s validated relying party ID (rp_id). An attacker could exploit this to spoof the user into providing credentials under the impression that they are authenticating to the top-level site.
Affected files:
ios/chrome/browser/passwords/bottom_sheet/coordinator/credential_suggestion_bottom_sheet_coordinator.mmios/chrome/browser/passwords/bottom_sheet/coordinator/passkey_suggestion_bottom_sheet_mediator.mmios/chrome/browser/passwords/bottom_sheet/coordinator/credential_suggestion_bottom_sheet_mediator_base.mmios/chrome/browser/passwords/bottom_sheet/ui/credential_suggestion_bottom_sheet_view_controller.mmcomponents/webauthn/ios/passkey_tab_helper.mmcomponents/webauthn/ios/passkey_java_script_feature.mm
Estimated timestamp from git blame: 2026-02-02
Description
There is a potential UI/origin spoofing vulnerability in the Google Password Manager (GPM) passkey assertion bottom sheet on iOS. When a cross-origin subframe initiates a modal passkey assertion (navigator.credentials.get()), the presented GPM bottom sheet displays the origin of the top-level frame instead of the requesting frame’s validated relying party identifier (rp_id).
While the companion passkey creation bottom sheet resolves and displays the validated rp_id via RelyingPartyIdForRequest(), the assertion (suggestion) flow instead uses the active WebState’s top-level URL to brand the UI.
Technical Details & Root Cause
-
Injection and Event Handling: The WebAuthn passkey shim is injected into all frames via
PasskeyJavaScriptFeature(components/webauthn/ios/passkey_java_script_feature.mm). When a subframe requests assertion vianavigator.credentials.get(),PasskeyTabHelper::HandleGetRequestedEventvalidates therp_idagainst the requesting frame’s security origin:// components/webauthn/ios/passkey_tab_helper.mm const url::Origin& origin = web_frame->GetSecurityOrigin(); const std::string& rp_id = params.RpId(); if (!OriginIsAllowedToClaimRelyingPartyId(rp_id, origin)) { ... } -
Omission of RP ID during Presentation: Once filtered, the helper calls
client_->ShowSuggestionBottomSheet(request_info). TheRequestInfostruct only contains frame identifiers, and the validatedrp_idis discarded:// components/webauthn/ios/ios_passkey_client.h struct RequestInfo { std::string frame_id; std::string request_id; std::optional<autofill::RemoteFrameToken> remote_frame_token; }; -
Branding with the Top-Level Origin:
CredentialSuggestionBottomSheetCoordinatorinstantiates the bottom sheet and extracts the display URL from the active top-level WebState:// ios/chrome/browser/passwords/bottom_sheet/coordinator/credential_suggestion_bottom_sheet_coordinator.mm WebStateList* webStateList = self.browser->GetWebStateList(); const GURL& URL = webStateList->GetActiveWebState()->GetLastCommittedURL(); // top-level page ... _viewController = [[CredentialSuggestionBottomSheetViewController alloc] initWithHandler:self URL:URL];The mediator base (
credential_suggestion_bottom_sheet_mediator_base.mm) also captures the active WebState’s top-level URL as_domainand propagates it to the consumer.PasskeySuggestionBottomSheetMediatordoes not retrieve the correctrp_idvia the tab helper (unlike the creation flow, which usesRelyingPartyIdForRequestto resolve and display the subframe’s target domain). -
Rendering in UI:
CredentialSuggestionBottomSheetViewController.mmprocesses the top-level URL to format the sheet’s header subtitle and row subtitles:// ios/chrome/browser/passwords/bottom_sheet/ui/credential_suggestion_bottom_sheet_view_controller.mm std::u16string formattedURL = url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains(_URL); self.subtitleString = l10n_util::GetNSStringF( IsConditionalPasskeyLoginEnabled() ? IDS_IOS_CREDENTIAL_BOTTOM_SHEET_SUBTITLE_WITH_PASSKEYS : IDS_IOS_CREDENTIAL_BOTTOM_SHEET_SUBTITLE, formattedURL);As a consequence, the relying party identity (
rp_id) of the subframe requesting the credential is never presented to the user. Instead, the trusted UI claims the authentication is for the top-level site.
Potential Attack Scenario
- The attacker registers a GPM passkey for
https://attacker.exampleon the victim’s device. - The attacker embeds
https://attacker.exampleas a cross-origin<iframe>insidehttps://victim.example(e.g., via compromised ad spaces or third-party widgets). - Inside the iframe, the attacker’s script initiates a modal WebAuthn assertion:
navigator.credentials.get({publicKey:{rpId:'attacker.example', challenge:new Uint8Array(32), allowCredentials:[]}}). - If successful, Chrome will display the GPM bottom sheet with the subtitle “You’ll sign in to victim.example” (or “Sign in to victim.example”), and each passkey suggestion row will list
victim.example(or a generic “Passkey” label) as the subtitle, spoofing the true destination. - The user selects the credential, passes Face ID/Touch ID, and a valid cryptographic assertion for
attacker.exampleis completed and returned to the attacker’s iframe.
(Note: These are potential steps; our tooling does not currently have the capability to execute proof-of-concept code to verify runtime exploitability.)
Suggested Fix
Update PasskeySuggestionBottomSheetMediator to resolve and display the validated rp_id of the requesting frame rather than defaulting to the top-level active WebState’s URL. This can be achieved by utilizing RelyingPartyIdForRequest() on PasskeyTabHelper to retrieve and format the correct domain for display, similar to the fix already implemented in PasskeyCreationBottomSheetMediator.
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.