Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Chrome for iOS
DescriptionIncorrect security UI in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker518049812
Fix commit543f13cf91a2 (chromium/src) +88/-132
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
components/webauthn/ios/passkey_java_script_feature.mm
modified

Files Changed

  • components/webauthn/ios/passkey_java_script_feature.h
  • components/webauthn/ios/passkey_java_script_feature.mm
  • components/webauthn/ios/passkey_tab_helper.h
From 543f13cf91a234e8f4f6959e3a3506cd29dca313 Mon Sep 17 00:00:00 2001
From: Alexis Hétu <sugoi@chromium.org>
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 <sugoi@chromium.org>
Reviewed-by: Sourav Uttam Sinha <sinhasourav@google.com>
Reviewed-by: Tommy Martino <tmartino@chromium.org>
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.