Chrome · Chrome for iOS
CVE-2026-17965
Logic Error in Chrome for iOS
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/webauthn/ios/passkey_java_script_feature.mm |
modified |
Files Changed
components/webauthn/ios/passkey_java_script_feature.hcomponents/webauthn/ios/passkey_java_script_feature.mmcomponents/webauthn/ios/passkey_tab_helper.h
Patch
From 543f13cf91a234e8f4f6959e3a3506cd29dca313 Mon Sep 17 00:00:00 2001 From: Alexis Hétu <[email protected]> Date: Thu, 04 Jun 2026 11:15:00 -0700 Subject: [PATCH] [iOS] Move incognito passkey creation warning after RP ID validation Resolve a security vulnerability where the incognito creation warning interstitial was presented to the user before validating the relying party identifier (RP ID). Previously, the interstitial was triggered in PasskeyJavaScriptFeature upon receiving a script message, before performing origin and RP ID validation. This allowed unauthorized origins to spoof browser UI warnings and trigger unwanted user interactions. This CL refactors the flow to move all interstitial logic and presentation into PasskeyTabHelper, ensuring the OTR warning is shown only during HandleRegistration after strict OriginIsAllowedToClaimRelyingPartyId checks and remote validation have successfully passed. Bug: 518049812 Change-Id: I0aad88464674e5df5f4d7926ead0f8900f9e81e6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7889746 Commit-Queue: Alexis Hétu <[email protected]> Reviewed-by: Sourav Uttam Sinha <[email protected]> Reviewed-by: Tommy Martino <[email protected]> Cr-Commit-Position: refs/heads/main@{#1641800} --- diff --git a/components/webauthn/ios/passkey_java_script_feature.h b/components/webauthn/ios/passkey_java_script_feature.h index 9512d73..f17690b 100644 --- a/components/webauthn/ios/passkey_java_script_feature.h +++ b/components/webauthn/ios/passkey_java_script_feature.h @@ -6,7 +6,6 @@ #define COMPONENTS_WEBAUTHN_IOS_PASSKEY_JAVA_SCRIPT_FEATURE_H_ #import "base/no_destructor.h" -#import "base/values.h" #import "components/webauthn/core/browser/passkey_model_utils.h" #import "components/webauthn/ios/passkey_request_params.h" #import "ios/web/public/js_messaging/java_script_feature.h" @@ -100,17 +99,6 @@ std::optional<std::string> GetScriptMessageHandlerName() const override; void ScriptMessageReceived(web::WebState* web_state, const web::ScriptMessage& message) override; - - // Continues the creation request flow after the Incognito check has passed. - void ProcessCreateRequest(web::WebState* web_state, - IOSPasskeyClient::RequestInfo request_info, - base::DictValue dict); - - // Callback handling the user's decision from the interstitial. - void OnInterstitialDecision(base::WeakPtr<web::WebState> web_state, - IOSPasskeyClient::RequestInfo request_info, - base::DictValue dict, - bool proceed); }; } // namespace webauthn diff --git a/components/webauthn/ios/passkey_java_script_feature.mm b/components/webauthn/ios/passkey_java_script_feature.mm index 09b85b82..d0724eb05 100644 --- a/components/webauthn/ios/passkey_java_script_feature.mm +++ b/components/webauthn/ios/passkey_java_script_feature.mm @@ -305,15 +305,35 @@ } if (is_handle_create_request_event) { - // base::Unretained is safe because this is a singleton. - if (passkey_tab_helper->ShowCreationInterstitialIfNecessary( - base::BindOnce(&PasskeyJavaScriptFeature::OnInterstitialDecision, - base::Unretained(this), web_state->GetWeakPtr(), - *request_info, dict.Clone()))) { + auto registration_request_params = + BuildRegistrationRequestParams(*request_info, dict); + + if (!registration_request_params.has_value()) { + base::UmaHistogramEnumeration("WebAuthentication.IOS.PasskeyParsingError", + registration_request_params.error()); + passkey_tab_helper->DeferToRenderer( + std::move(*request_info), + PasskeyRequestParams::RequestType::kUnknown); return; } - ProcessCreateRequest(web_state, std::move(*request_info), dict.Clone()); + if (!ValidateFeatureUsage(*registration_request_params)) { + // TODO(crbug.com/460485333): Log the error. + passkey_tab_helper->DeferToRenderer(std::move(*request_info), + registration_request_params->Type()); + return; + } + + // Passkey creation is only allowed if it originates from a user gesture. + if (!message.is_user_interacting()) { + // TODO(crbug.com/460485333): Log the error. + passkey_tab_helper->DeferToRenderer(std::move(*request_info), + registration_request_params->Type()); + return; + } + + passkey_tab_helper->HandleCreateRequestedEvent( + std::move(*registration_request_params)); return; } @@ -330,7 +350,7 @@ } if (!ValidateFeatureUsage(*assertion_request_params)) { - // TODO(460485333): Log the error. + // TODO(crbug.com/460485333): Log the error. passkey_tab_helper->DeferToRenderer(std::move(*request_info), assertion_request_params->Type()); return; @@ -341,60 +361,4 @@ } } -void PasskeyJavaScriptFeature::OnInterstitialDecision( - base::WeakPtr<web::WebState> web_state, - IOSPasskeyClient::RequestInfo request_info, - base::DictValue dict, - bool proceed) { - if (!web_state) { - return; - } - - if (!proceed) { - web::WebFramesManager* frames_manager = - GetWebFramesManager(web_state.get()); - web::WebFrame* frame = - frames_manager->GetFrameWithId(request_info.frame_id); - if (frame) { - RejectPasskeyRequest(frame, request_info.request_id); - } - return; - } - - ProcessCreateRequest(web_state.get(), std::move(request_info), - std::move(dict)); -} - -void PasskeyJavaScriptFeature::ProcessCreateRequest( - web::WebState* web_state, - IOSPasskeyClient::RequestInfo request_info, - base::DictValue dict) { - PasskeyTabHelper* passkey_tab_helper = - PasskeyTabHelper::FromWebState(web_state); - if (!passkey_tab_helper) { - return; - } - - auto registration_request_params = - BuildRegistrationRequestParams(request_info, dict); - - if (!registration_request_params.has_value()) { - base::UmaHistogramEnumeration("WebAuthentication.IOS.PasskeyParsingError", - registration_request_params.error()); - passkey_tab_helper->DeferToRenderer( - std::move(request_info), PasskeyRequestParams::RequestType::kUnknown); - return; - } - - if (!ValidateFeatureUsage(*registration_request_params)) { - // TODO(460485333): Log the error. - passkey_tab_helper->DeferToRenderer(std::move(request_info), - registration_request_params->Type()); - return; - } - - passkey_tab_helper->HandleCreateRequestedEvent( - std::move(*registration_request_params)); -} - } // namespace webauthn diff --git a/components/webauthn/ios/passkey_tab_helper.h b/components/webauthn/ios/passkey_tab_helper.h index 57fa512d..6c4f2361 100644 --- a/components/webauthn/ios/passkey_tab_helper.h +++ b/components/webauthn/ios/passkey_tab_helper.h @@ -127,10 +127,6 @@ // Returns whether there is a pending remote validation for testing. bool HasPendingValidationForTesting() const; - // Returns whether the interstitial is necessary for the current state. - bool ShowCreationInterstitialIfNecessary( - base::OnceCallback<void(bool)> callback); - private: friend class web::WebStateUserData<PasskeyTabHelper>; friend class PasskeyTabHelperTest; @@ -216,6 +212,13 @@ // Handles passkey registration requests after it passes validation. void HandleRegistration(RegistrationRequestParams params); + // Initiates the passkey registration flow, showing the incognito warning + // interstitial first if the browser state is off-the-record. + void MaybeShowInterstitialAndRegister(RegistrationRequestParams params); + + // Callback handling the user's decision from the interstitial. + void OnInterstitialDecision(RegistrationRequestParams params, bool proceed);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/webauthn/ios/passkey_tab_helper_unittest.mm b/components/webauthn/ios/passkey_tab_helper_unittest.mm
index 317ae9bd3..568fcbd 100644
--- a/components/webauthn/ios/passkey_tab_helper_unittest.mm
+++ b/components/webauthn/ios/passkey_tab_helper_unittest.mm
@@ -151,6 +151,10 @@
return PasskeyTabHelper::FromWebState(&fake_web_state_);
}
+ void MaybeShowInterstitialAndRegister(RegistrationRequestParams params) {
+ passkey_tab_helper()->MaybeShowInterstitialAndRegister(std::move(params));
+ }
+
bool HasExcludedPasskey(const RegistrationRequestParams& params) {
return passkey_tab_helper()->HasExcludedPasskey(params);
}
@@ -565,71 +569,43 @@
}
}
-TEST_F(PasskeyTabHelperTest, ShowCreationInterstitialAndContinue) {
+TEST_F(PasskeyTabHelperTest, MaybeShowInterstitialAndRegisterAndContinue) {
fake_browser_state_.SetOffTheRecord(true);
-
- bool callback_executed = false;
- bool callback_result = false;
- auto callback = base::BindOnce(
- [](bool* executed, bool* result, bool proceed) {
- *executed = true;
- *result = proceed;
- },
- &callback_executed, &callback_result);
+ SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
client_->SetInterstitialProceeds(true);
- EXPECT_TRUE(passkey_tab_helper()->ShowCreationInterstitialIfNecessary(
- std::move(callback)));
+ MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));
EXPECT_TRUE(client_->DidShowInterstitial());
- EXPECT_TRUE(callback_executed);
- EXPECT_TRUE(callback_result);
+ EXPECT_TRUE(client_->DidShowCreationBottomSheet());
histogram_tester_.ExpectUniqueSample(
kWebAuthenticationIOSContentAreaEventHistogram,
static_cast<int>(kIncognitoInterstitialShown),
/*count=*/1);
}
-TEST_F(PasskeyTabHelperTest, ShowCreationInterstitialAndCancel) {
+TEST_F(PasskeyTabHelperTest, MaybeShowInterstitialAndRegisterAndCancel) {
fake_browser_state_.SetOffTheRecord(true);
-
- bool callback_executed = false;
- bool callback_result = true;
- auto callback = base::BindOnce(
- [](bool* executed, bool* result, bool proceed) {
- *executed = true;
- *result = proceed;
- },
- &callback_executed, &callback_result);
+ SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
client_->SetInterstitialProceeds(false);
- EXPECT_TRUE(passkey_tab_helper()->ShowCreationInterstitialIfNecessary(
- std::move(callback)));
+ MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));
EXPECT_TRUE(client_->DidShowInterstitial());
- EXPECT_TRUE(callback_executed);
- EXPECT_FALSE(callback_result);
+ EXPECT_FALSE(client_->DidShowCreationBottomSheet());
histogram_tester_.ExpectUniqueSample(
kWebAuthenticationIOSContentAreaEventHistogram,
static_cast<int>(kIncognitoInterstitialShown),
/*count=*/1);
}
-TEST_F(PasskeyTabHelperTest, NoCreationInterstitial) {
+TEST_F(PasskeyTabHelperTest, NoInterstitial) {
fake_browser_state_.SetOffTheRecord(false);
+ SetUpWebFramesManagerAndWebFrame(GURL(kOriginURL));
- bool callback_executed = false;
- auto callback =
- base::BindOnce([](bool* executed, bool proceed) { *executed = true; },
- &callback_executed);
-
- EXPECT_FALSE(passkey_tab_helper()->ShowCreationInterstitialIfNecessary(
- std::move(callback)));
-
+ MaybeShowInterstitialAndRegister(BuildRegistrationRequestParams({}));
EXPECT_FALSE(client_->DidShowInterstitial());
- EXPECT_FALSE(callback_executed);
- histogram_tester_.ExpectTotalCount(
- kWebAuthenticationIOSContentAreaEventHistogram, 0);
+ EXPECT_TRUE(client_->DidShowCreationBottomSheet());
}
TEST_F(PasskeyTabHelperTest, HandleRegistrationDefersWhenGpmDisabled) {
diff --git a/ios/chrome/browser/webauthn/test/passkey_egtest.mm b/ios/chrome/browser/webauthn/test/passkey_egtest.mm
index aeb08a0..ef3e244f 100644
--- a/ios/chrome/browser/webauthn/test/passkey_egtest.mm
+++ b/ios/chrome/browser/webauthn/test/passkey_egtest.mm
@@ -96,6 +96,7 @@
"/navigator_credentials_create.html");
[ChromeEarlGrey loadURL:pageURL];
[ChromeEarlGrey waitForWebStateContainingText:"Credential Create Test Page"];
+ [ChromeEarlGrey tapWebStateElementWithID:@"create-passkey-btn"];
}
- (void)loadPasskeyCancelPage {
@@ -103,6 +104,7 @@
"/navigator_credentials_cancel.html");
[ChromeEarlGrey loadURL:pageURL];
[ChromeEarlGrey waitForWebStateContainingText:"Credential Cancel Test Page"];
+ [ChromeEarlGrey tapWebStateElementWithID:@"create-passkey-btn"];
}
#pragma mark - Tests
diff --git a/ios/testing/data/http_server_files/navigator_credentials_cancel.html b/ios/testing/data/http_server_files/navigator_credentials_cancel.html
index 6ebfa56b..c036d3c 100644
--- a/ios/testing/data/http_server_files/navigator_credentials_cancel.html
+++ b/ios/testing/data/http_server_files/navigator_credentials_cancel.html
@@ -1,5 +1,6 @@
<html><body>
<p>Credential Cancel Test Page</p>
+<button id="create-passkey-btn" onclick="triggerCreate()">Create Passkey</button>
<script>
const controller = new AbortController();
const mockCredentialCreationOptions = {
@@ -34,13 +35,16 @@
],
attestation: 'none',
};
-navigator.credentials.create({
- publicKey: mockCredentialCreationOptions,
- signal: controller.signal
-});
+
+function triggerCreate() {
+ navigator.credentials.create({
+ publicKey: mockCredentialCreationOptions,
+ signal: controller.signal
+ });
+}
+
window.triggerAbort = function() {
controller.abort();
};
</script>
</body></html>
-
diff --git a/ios/testing/data/http_server_files/navigator_credentials_create.html b/ios/testing/data/http_server_files/navigator_credentials_create.html
index a8f2f54d..88da1c95 100644
--- a/ios/testing/data/http_server_files/navigator_credentials_create.html
+++ b/ios/testing/data/http_server_files/navigator_credentials_create.html
@@ -1,6 +1,7 @@
<html><body>
<p>Credential Create Test Page</p>
+<button id="create-passkey-btn" onclick="triggerCreate()">Create Passkey</button>
<script>
const mockCredentialCreationOptions = {
@@ -35,7 +36,10 @@
],
attestation: 'none',
};
-navigator.credentials.create({publicKey: mockCredentialCreationOptions});
+
+function triggerCreate() {
+ navigator.credentials.create({publicKey: mockCredentialCreationOptions});
+}
</script>
</body></html>
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page