Overview

Low
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Contacts
DescriptionMissing authorization in Contacts
ComponentContacts
Sub-componentContacts
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker533084499
Fix commit460810f61f17 (chromium/src) +205/-10
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-08-06 (public git)
Disclosed2026-09-08
Patch gap33 days — fix public in source before the advisory

Background

`ContactsManagerImpl`
a browser-side DocumentService<blink::mojom::ContactsManager> bound to a single RenderFrameHost’s document that services the renderer’s Contacts Picker API requests.
`DocumentService`
a Mojo interface implementation whose lifetime is tied to a document; it stays alive but inactive when the document enters the Back-Forward Cache (BFCache) rather than being torn down.
Back-Forward Cache (BFCache)
a mechanism that keeps a navigated-away-from document alive and inactive so it can be instantly restored, meaning its RenderFrameHost is no longer the active, visible frame.
`ContactsProvider::Select`
the downstream call that actually launches the platform’s fullscreen Contacts Picker dialog and returns the chosen contacts.

Root Cause Analysis

The browser-side ContactsManagerImpl::Select method received Select requests over the Mojo pipe and forwarded them directly to contacts_provider_->Select(...) without checking whether the calling document was still the active, visible page. Because ContactsManagerImpl is a DocumentService, its receiver remains bound and functional after the document is pushed into the BFCache or when it belongs to a subframe, so an inactive or non-top-level frame could still issue Select and cause the fullscreen picker to appear over an unrelated page the user is actually looking at. The invariant violated is that a UI-privileged, page-modal action must originate only from the active primary main frame.

The fix adds an early guard that calls render_frame_host().IsActive() and render_frame_host().IsInPrimaryMainFrame(); if either is false it runs std::move(mojom_callback).Run(std::nullopt) and returns before ever touching contacts_provider_, so BFCached and subframe requests are rejected. Returning std::nullopt rather than terminating the renderer keeps the response safe against benign races such as a navigation firing immediately after the API call.

Key insight
The single mistake was omitting an active-and-primary-main-frame authorization check in a DocumentService whose Select handler stays reachable while the document is inactive; the fix restores that check as an early return that short-circuits before the provider is invoked.

Attack Path

  1. Bind the interface from a backgroundable page A page (or a subframe on it) obtains a live blink::mojom::ContactsManager remote whose ContactsManagerImpl is bound to its document.
  2. Move the document out of context The page navigates away so its document enters the BFCache (staying alive but inactive), or the request originates from a non-primary subframe rather than the top-level frame.
  3. Invoke `Select` while inactive The inactive/subframe renderer sends a Select request; pre-patch, ContactsManagerImpl::Select skipped straight to contacts_provider_->Select(...) with no active-frame check.
  4. Picker launches over the wrong page The fullscreen Contacts Picker dialog is displayed on top of the unrelated, currently visible page, enabling a UI-spoofing / unauthorized-prompt condition where the user may share contact data believing it belongs to the foreground site (inferred that user data disclosure results from a successful share).

Impact Assessment

An attacker-controlled page can spawn the fullscreen Contacts Picker from a document that is no longer the active, visible frame (a BFCached page or a subframe), displaying a contacts-sharing prompt over an unrelated top-level page in the browser UI context. The precondition is that the victim has an origin able to bind the Contacts Manager interface and the ability to background it into BFCache or run it as a subframe; the advisory classifies this as a missing-authorization bug in Contacts and does not report exploitation in the wild. Any resulting theft of the user’s contact data through the misattributed prompt is (inferred) rather than demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
content/browser/contacts/contacts_manager_impl.cc
modified
RenderFrameHost
content/browser/contacts/contacts_manager_impl.h
modified
ContactsManagerImpl
content/browser/contacts/contacts_manager_impl.h
modified
CONTENT_EXPORT
content/browser/contacts/contacts_manager_impl.h
modified
MockContactsProvider
content/browser/contacts/contacts_manager_impl_unittest.cc
modified
ContactsManagerImplTest
content/browser/contacts/contacts_manager_impl_unittest.cc
modified
TEST_F
content/browser/contacts/contacts_manager_impl_unittest.cc
modified

Files Changed

  • content/browser/contacts/contacts_manager_impl_unittest.cc
  • content/browser/contacts/contacts_manager_impl.cc
  • content/test/BUILD.gn
  • content/browser/contacts/contacts_manager_impl.h

Audit Directions

  • Unguarded `DocumentService` handlers
    Review other DocumentService implementations whose Mojo methods trigger UI or privileged actions and confirm each checks IsActive() / IsInPrimaryMainFrame() before acting, since these objects survive in BFCache.
  • Page-modal UI from non-primary frames
    Audit any browser-side surface that opens fullscreen or page-modal dialogs (pickers, permission prompts, choosers) to ensure the request is rejected when it does not come from the active primary main frame.
  • Callback-safe rejection paths
    Where a frame-liveness guard is added, verify the early exit still fulfills the Mojo callback (e.g. with std::nullopt) rather than dropping it, to avoid renderer hangs or crashes on navigation races.
From 460810f61f17c448f9866e72bc00b847b58dd015 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <robpitkin@google.com>
Date: Thu, 06 Aug 2026 10:49:18 -0700
Subject: [PATCH] contacts: Prevent out-of-context launches from BFCache and subframes.

ContactsManagerImpl (a DocumentService) is owned by the document of a
RenderFrameHost. If the document enters the Back-Forward Cache
(BFCache), it remains alive but inactive. The browser-side
ContactsManagerImpl::Select method did not check if the requesting frame
was active and in the primary main frame, which allowed an inactive
frame to trigger the fullscreen Contacts Picker dialog over an unrelated
visible page.

This CL adds checks to verify that the requesting RenderFrameHost is
active and in the primary main frame. If not, the request is safely
rejected by returning std::nullopt via the callback to avoid crashing
the renderer in case of race conditions (e.g. navigation immediately
after API call).

To support this validation, this CL introduces:
- A SetContactsProviderForTesting API to ContactsManagerImpl.
- A unit test suite (contacts_manager_impl_unittest.cc) using a MockContactsProvider
  to verify that:
  1. Active primary main frames can launch the picker.
  2. BFCached frames safely reject the request and do not invoke the provider.
  3. Subframes safely reject the request and do not invoke the provider.

Bug: 533084499
Test: content_unittests --gtest_filter=ContactsManagerImplTest.*
Change-Id: I79521ea0fc4af692d95083a2fdeb9385506e9765
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8178783
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Rob Pitkin <robpitkin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1675096}
---
diff --git a/content/browser/contacts/contacts_manager_impl.cc b/content/browser/contacts/contacts_manager_impl.cc
index 26918f13..4238bde 100644
--- a/content/browser/contacts/contacts_manager_impl.cc
+++ b/content/browser/contacts/contacts_manager_impl.cc
@@ -54,11 +54,31 @@
 
 }  // namespace
 
+// static
+void ContactsManagerImpl::Create(
+    RenderFrameHost* render_frame_host,
+    mojo::PendingReceiver<blink::mojom::ContactsManager> receiver) {
+  CHECK(render_frame_host);
+  new ContactsManagerImpl(*render_frame_host, std::move(receiver),
+                          CreateProvider(*render_frame_host));
+}
+
+// static
+ContactsManagerImpl* ContactsManagerImpl::CreateForTesting(
+    RenderFrameHost* render_frame_host,
+    mojo::PendingReceiver<blink::mojom::ContactsManager> receiver,
+    std::unique_ptr<ContactsProvider> provider) {
+  CHECK(render_frame_host);
+  return new ContactsManagerImpl(*render_frame_host, std::move(receiver),
+                                 std::move(provider));
+}
+
 ContactsManagerImpl::ContactsManagerImpl(
     RenderFrameHost& render_frame_host,
-    mojo::PendingReceiver<blink::mojom::ContactsManager> receiver)
+    mojo::PendingReceiver<blink::mojom::ContactsManager> receiver,
+    std::unique_ptr<ContactsProvider> provider)
     : DocumentService(render_frame_host, std::move(receiver)),
-      contacts_provider_(CreateProvider(render_frame_host)) {
+      contacts_provider_(std::move(provider)) {
   CHECK(!render_frame_host.IsInLifecycleState(
       RenderFrameHost::LifecycleState::kPrerendering));
   source_id_ = render_frame_host.GetPageUkmSourceId();
@@ -73,6 +93,12 @@
                                  bool include_addresses,
                                  bool include_icons,
                                  SelectCallback mojom_callback) {
+  if (!render_frame_host().IsActive() ||
+      !render_frame_host().IsInPrimaryMainFrame()) {
+    std::move(mojom_callback).Run(std::nullopt);
+    return;
+  }
+
   if (contacts_provider_) {
     contacts_provider_->Select(
         multiple, include_names, include_emails, include_tel, include_addresses,
diff --git a/content/browser/contacts/contacts_manager_impl.h b/content/browser/contacts/contacts_manager_impl.h
index 5ada2c1c..d5ad7eb6 100644
--- a/content/browser/contacts/contacts_manager_impl.h
+++ b/content/browser/contacts/contacts_manager_impl.h
@@ -6,6 +6,7 @@
 #define CONTENT_BROWSER_CONTACTS_CONTACTS_MANAGER_IMPL_H_
 
 #include "content/browser/contacts/contacts_provider.h"
+#include "content/common/content_export.h"
 #include "content/public/browser/document_service.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "third_party/blink/public/mojom/contacts/contacts_manager.mojom.h"
@@ -14,17 +15,17 @@
 
 class RenderFrameHost;
 
-class ContactsManagerImpl
+class CONTENT_EXPORT ContactsManagerImpl
     : public DocumentService<blink::mojom::ContactsManager> {
  public:
   static void Create(
       RenderFrameHost* render_frame_host,
-      mojo::PendingReceiver<blink::mojom::ContactsManager> receiver) {
-    CHECK(render_frame_host);
-    // The object is bound to the lifetime of `render_frame_host`'s logical
-    // document by virtue of being a `DocumentService` implementation.
-    new ContactsManagerImpl(*render_frame_host, std::move(receiver));
-  }
+      mojo::PendingReceiver<blink::mojom::ContactsManager> receiver);
+
+  static ContactsManagerImpl* CreateForTesting(
+      RenderFrameHost* render_frame_host,
+      mojo::PendingReceiver<blink::mojom::ContactsManager> receiver,
+      std::unique_ptr<ContactsProvider> provider);
 
   ContactsManagerImpl(const ContactsManagerImpl&) = delete;
   ContactsManagerImpl& operator=(const ContactsManagerImpl&) = delete;
@@ -42,7 +43,8 @@
  private:
   explicit ContactsManagerImpl(
       RenderFrameHost& render_frame_host,
-      mojo::PendingReceiver<blink::mojom::ContactsManager> receiver);
+      mojo::PendingReceiver<blink::mojom::ContactsManager> receiver,
+      std::unique_ptr<ContactsProvider> provider);
 
   std::unique_ptr<ContactsProvider> contacts_provider_;
 
diff --git a/content/browser/contacts/contacts_manager_impl_unittest.cc b/content/browser/contacts/contacts_manager_impl_unittest.cc
new file mode 100644
index 0000000..80df5ac5
--- /dev/null
+++ b/content/browser/contacts/contacts_manager_impl_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/contacts/contacts_manager_impl.h"
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/gmock_callback_support.h"
+#include "base/test/mock_callback.h"
+#include "base/test/test_future.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/contacts/contacts_manager.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockContactsProvider : public ContactsProvider {
+ public:
+  MockContactsProvider() = default;
+  ~MockContactsProvider() override = default;
+
+  MOCK_METHOD(void,
+              Select,
+              (bool multiple,
+               bool include_names,
+               bool include_emails,
+               bool include_tel,
+               bool include_addresses,
+               bool include_icons,
+               ContactsProvider::ContactsSelectedCallback callback),
+              (override));
+};
+
+}  // namespace
+
+class ContactsManagerImplTest : public RenderViewHostImplTestHarness {
+ public:
+  ContactsManagerImplTest() = default;
+  ~ContactsManagerImplTest() override = default;
+
+  void SetUp() override {
+    RenderViewHostImplTestHarness::SetUp();
+    RenderFrameHostTester::For(main_rfh())->InitializeRenderFrameIfNeeded();
+    NavigateAndCommit(GURL("https://example.com"));
+  }
+
+  void TearDown() override {
+    contacts_manager_impl_ = nullptr;
+    RenderViewHostImplTestHarness::TearDown();
+  }
+
+  void InitService(std::unique_ptr<ContactsProvider> provider) {
+    contacts_manager_impl_ = ContactsManagerImpl::CreateForTesting(
+        main_rfh(), contacts_manager_remote_.BindNewPipeAndPassReceiver(),
+        std::move(provider));
+  }
+
+  mojo::Remote<blink::mojom::ContactsManager>& remote() {
+    return contacts_manager_remote_;
+  }
+
+ private:
+  mojo::Remote<blink::mojom::ContactsManager> contacts_manager_remote_;
+  raw_ptr<ContactsManagerImpl> contacts_manager_impl_ = nullptr;
+};
+
+TEST_F(ContactsManagerImplTest, SelectActivePrimaryMainFrame) {
+  auto mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* mock_provider_ptr = mock_provider.get();
+
+  // Set up expectation on mock provider.
+  std::vector<blink::mojom::ContactInfoPtr> expected_contacts;
+  auto contact = blink::mojom::ContactInfo::New();
+  contact->name = std::vector<std::string>{"John Doe"};
+  expected_contacts.push_back(std::move(contact));
+
+  EXPECT_CALL(*mock_provider_ptr, Select)
+      .WillOnce(base::test::RunOnceCallback<6>(std::move(expected_contacts),
+                                               /*percentage_shared=*/100,
+                                               ContactsPickerProperties()));
+
+  InitService(std::move(mock_provider));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  remote()->Select(/*multiple=*/false, /*include_names=*/true,
+                   /*include_emails=*/false, /*include_tel=*/false,
+                   /*include_addresses=*/false, /*include_icons=*/false,
+                   future.GetCallback());
+
+  ASSERT_TRUE(future.Get().has_value());
+  EXPECT_EQ(future.Get()->size(), 1u);
+  EXPECT_EQ(future.Get()->at(0)->name->at(0), "John Doe");
+}
+
+TEST_F(ContactsManagerImplTest, SelectBFCachedFrame) {
+  auto mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* mock_provider_ptr = mock_provider.get();
+
+  EXPECT_CALL(*mock_provider_ptr, Select).Times(0);
+
+  InitService(std::move(mock_provider));
+
+  // Put the frame into BFCache.
+  static_cast<TestRenderFrameHost*>(main_rfh())->DidEnterBackForwardCache();
+  EXPECT_TRUE(main_rfh()->IsInLifecycleState(
+      RenderFrameHost::LifecycleState::kInBackForwardCache));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  // Call Select. It should return nullopt safely.
+  remote()->Select(/*multiple=*/false, /*include_names=*/true,
+                   /*include_emails=*/false, /*include_tel=*/false,
+                   /*include_addresses=*/false, /*include_icons=*/false,
+                   future.GetCallback());
+
+  EXPECT_EQ(future.Get(), std::nullopt);
+}
+
+TEST_F(ContactsManagerImplTest, SelectSubframe) {
+  // Create a subframe.
+  RenderFrameHost* subframe =
+      RenderFrameHostTester::For(main_rfh())->AppendChild("subframe");
+  ASSERT_TRUE(subframe);
+  RenderFrameHostTester::For(subframe)->InitializeRenderFrameIfNeeded();
+
+  // Navigate the subframe.
+  auto navigation = NavigationSimulator::CreateRendererInitiated(
+      GURL("https://example.com/subframe"), subframe);
+  navigation->Commit();
+  subframe = navigation->GetFinalRenderFrameHost();
+
+  mojo::Remote<blink::mojom::ContactsManager> subframe_remote;
+  auto subframe_mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* subframe_mock_provider_ptr =
+      subframe_mock_provider.get();
+
+  EXPECT_CALL(*subframe_mock_provider_ptr, Select).Times(0);
+
+  ContactsManagerImpl::CreateForTesting(
+      subframe, subframe_remote.BindNewPipeAndPassReceiver(),
+      std::move(subframe_mock_provider));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  // Call Select on subframe. It should return nullopt safely because it is not
+  // a main frame.
+  subframe_remote->Select(/*multiple=*/false, /*include_names=*/true,
+                          /*include_emails=*/false, /*include_tel=*/false,
+                          /*include_addresses=*/false, /*include_icons=*/false,
+                          future.GetCallback());
+
+  EXPECT_EQ(future.Get(), std::nullopt);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 35124eb..5e75e2c93 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2606,6 +2606,7 @@
     "../browser/code_cache/dedicated_task_runner_for_resource_unittest.cc",
     "../browser/code_cache/generated_code_cache_unittest.cc",
     "../browser/code_cache/simple_lru_cache_unittest.cc",
+    "../browser/contacts/contacts_manager_impl_unittest.cc",
     "../browser/content_index/content_index_database_unittest.cc",
     "../browser/content_index/content_index_service_impl_unittest.cc",
     "../browser/cookie_store/cookie_store_manager_unittest.cc",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/contacts/contacts_manager_impl_unittest.cc b/content/browser/contacts/contacts_manager_impl_unittest.cc
new file mode 100644
index 0000000..80df5ac5
--- /dev/null
+++ b/content/browser/contacts/contacts_manager_impl_unittest.cc
@@ -0,0 +1,166 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/contacts/contacts_manager_impl.h"
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/gmock_callback_support.h"
+#include "base/test/mock_callback.h"
+#include "base/test/test_future.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_render_frame_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/contacts/contacts_manager.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockContactsProvider : public ContactsProvider {
+ public:
+  MockContactsProvider() = default;
+  ~MockContactsProvider() override = default;
+
+  MOCK_METHOD(void,
+              Select,
+              (bool multiple,
+               bool include_names,
+               bool include_emails,
+               bool include_tel,
+               bool include_addresses,
+               bool include_icons,
+               ContactsProvider::ContactsSelectedCallback callback),
+              (override));
+};
+
+}  // namespace
+
+class ContactsManagerImplTest : public RenderViewHostImplTestHarness {
+ public:
+  ContactsManagerImplTest() = default;
+  ~ContactsManagerImplTest() override = default;
+
+  void SetUp() override {
+    RenderViewHostImplTestHarness::SetUp();
+    RenderFrameHostTester::For(main_rfh())->InitializeRenderFrameIfNeeded();
+    NavigateAndCommit(GURL("https://example.com"));
+  }
+
+  void TearDown() override {
+    contacts_manager_impl_ = nullptr;
+    RenderViewHostImplTestHarness::TearDown();
+  }
+
+  void InitService(std::unique_ptr<ContactsProvider> provider) {
+    contacts_manager_impl_ = ContactsManagerImpl::CreateForTesting(
+        main_rfh(), contacts_manager_remote_.BindNewPipeAndPassReceiver(),
+        std::move(provider));
+  }
+
+  mojo::Remote<blink::mojom::ContactsManager>& remote() {
+    return contacts_manager_remote_;
+  }
+
+ private:
+  mojo::Remote<blink::mojom::ContactsManager> contacts_manager_remote_;
+  raw_ptr<ContactsManagerImpl> contacts_manager_impl_ = nullptr;
+};
+
+TEST_F(ContactsManagerImplTest, SelectActivePrimaryMainFrame) {
+  auto mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* mock_provider_ptr = mock_provider.get();
+
+  // Set up expectation on mock provider.
+  std::vector<blink::mojom::ContactInfoPtr> expected_contacts;
+  auto contact = blink::mojom::ContactInfo::New();
+  contact->name = std::vector<std::string>{"John Doe"};
+  expected_contacts.push_back(std::move(contact));
+
+  EXPECT_CALL(*mock_provider_ptr, Select)
+      .WillOnce(base::test::RunOnceCallback<6>(std::move(expected_contacts),
+                                               /*percentage_shared=*/100,
+                                               ContactsPickerProperties()));
+
+  InitService(std::move(mock_provider));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  remote()->Select(/*multiple=*/false, /*include_names=*/true,
+                   /*include_emails=*/false, /*include_tel=*/false,
+                   /*include_addresses=*/false, /*include_icons=*/false,
+                   future.GetCallback());
+
+  ASSERT_TRUE(future.Get().has_value());
+  EXPECT_EQ(future.Get()->size(), 1u);
+  EXPECT_EQ(future.Get()->at(0)->name->at(0), "John Doe");
+}
+
+TEST_F(ContactsManagerImplTest, SelectBFCachedFrame) {
+  auto mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* mock_provider_ptr = mock_provider.get();
+
+  EXPECT_CALL(*mock_provider_ptr, Select).Times(0);
+
+  InitService(std::move(mock_provider));
+
+  // Put the frame into BFCache.
+  static_cast<TestRenderFrameHost*>(main_rfh())->DidEnterBackForwardCache();
+  EXPECT_TRUE(main_rfh()->IsInLifecycleState(
+      RenderFrameHost::LifecycleState::kInBackForwardCache));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  // Call Select. It should return nullopt safely.
+  remote()->Select(/*multiple=*/false, /*include_names=*/true,
+                   /*include_emails=*/false, /*include_tel=*/false,
+                   /*include_addresses=*/false, /*include_icons=*/false,
+                   future.GetCallback());
+
+  EXPECT_EQ(future.Get(), std::nullopt);
+}
+
+TEST_F(ContactsManagerImplTest, SelectSubframe) {
+  // Create a subframe.
+  RenderFrameHost* subframe =
+      RenderFrameHostTester::For(main_rfh())->AppendChild("subframe");
+  ASSERT_TRUE(subframe);
+  RenderFrameHostTester::For(subframe)->InitializeRenderFrameIfNeeded();
+
+  // Navigate the subframe.
+  auto navigation = NavigationSimulator::CreateRendererInitiated(
+      GURL("https://example.com/subframe"), subframe);
+  navigation->Commit();
+  subframe = navigation->GetFinalRenderFrameHost();
+
+  mojo::Remote<blink::mojom::ContactsManager> subframe_remote;
+  auto subframe_mock_provider = std::make_unique<MockContactsProvider>();
+  MockContactsProvider* subframe_mock_provider_ptr =
+      subframe_mock_provider.get();
+
+  EXPECT_CALL(*subframe_mock_provider_ptr, Select).Times(0);
+
+  ContactsManagerImpl::CreateForTesting(
+      subframe, subframe_remote.BindNewPipeAndPassReceiver(),
+      std::move(subframe_mock_provider));
+
+  base::test::TestFuture<
+      std::optional<std::vector<blink::mojom::ContactInfoPtr>>>
+      future;
+  // Call Select on subframe. It should return nullopt safely because it is not
+  // a main frame.
+  subframe_remote->Select(/*multiple=*/false, /*include_names=*/true,
+                          /*include_emails=*/false, /*include_tel=*/false,
+                          /*include_addresses=*/false, /*include_icons=*/false,
+                          future.GetCallback());
+
+  EXPECT_EQ(future.Get(), std::nullopt);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 35124eb..5e75e2c93 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2606,6 +2606,7 @@
     "../browser/code_cache/dedicated_task_runner_for_resource_unittest.cc",
     "../browser/code_cache/generated_code_cache_unittest.cc",
     "../browser/code_cache/simple_lru_cache_unittest.cc",
+    "../browser/contacts/contacts_manager_impl_unittest.cc",
     "../browser/content_index/content_index_database_unittest.cc",
     "../browser/content_index/content_index_service_impl_unittest.cc",
     "../browser/cookie_store/cookie_store_manager_unittest.cc",
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.