Low watch CVSS 4.3 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Extensions
DescriptionIncorrect authorization in Extensions
ComponentExtensions
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.
Tracker513172858
Fix commit7955fc07e9ee (chromium/src) +166/-28
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-07-08 (public git)
Disclosed2026-08-25
Patch gap48 days — fix public in source before the advisory

Background

Glic private API (`glicPrivate`)
A privileged component-extension API in Chrome that lets an eligible signed-in surface invoke Glic (Gemini-in-Chrome) actions such as prompt invocation, addressed by a document_id.
`externally_connectable`
A manifest mechanism that lets specified web origins (here *.google.com) message an extension; it is frame-scoped, so a matching subframe can reach the extension even when the top-level frame cannot.
`RenderFrameHost::IsInPrimaryMainFrame()`
Returns true only when the frame is the primary (top-level, non-prerender, non-fenced) main frame of its tab, distinguishing it from subframes.
`GetRfhForDocumentId` / `IsAccountConsistent`
GetRfhForDocumentId resolves a caller-supplied document_id to a RenderFrameHost; IsAccountConsistent checks that the frame’s account matches the profile’s identity.

Root Cause Analysis

Three Glic private API entry points in glic_private_api.cc resolved the caller’s document_id to a RenderFrameHost and then only checked that rfh was non-null and that IsAccountConsistent held, before proceeding. Neither check constrained the frame to be the tab’s primary main frame, so a subframe’s RenderFrameHost was accepted. Because externally_connectable is frame-scoped, a *.google.com subframe embedded inside a non-connectable top-level page (e.g. example.com) can reach the Glic component extension and, via background.ts, forward its own subframe documentId into glicPrivate.invoke. The API therefore acted on a request originating from a subframe that is not the authoritative top-level Glic surface, violating the intended “primary main frame only” authorization.

The fix introduces ValidateRenderFrameHost, which returns kLocalInvalidDocumentId when !rfh || !rfh->IsInPrimaryMainFrame() and kLocalAccountMismatch on account inconsistency, and routes all three call sites through it so subframe callers are rejected before any action runs. The shipped browser test embeds a gemini.google.com subframe under example.com, invokes from the subframe, and expects local-invalid-document-id.

Key insight
The single mistake was validating only the existence and account consistency of the resolved frame while omitting a primary-main-frame check, letting a frame-scoped-connectable subframe drive a top-level-only API; ValidateRenderFrameHost’s IsInPrimaryMainFrame() gate closes it.

Attack Path

  1. Embed connectable subframe An attacker page at a non-connectable origin (example.com) embeds a *.google.com subframe (e.g. gemini.google.com), which, being frame-scoped-connectable, can reach the Glic component extension via chrome.runtime.sendMessage.
  2. Forward subframe documentId The subframe sends a glicPrivate.invoke message; the extension’s background.ts forwards sender.documentId — the subframe’s document — into chrome.glicPrivate.invoke.
  3. Missing frame-type check GetRfhForDocumentId resolves the subframe’s RenderFrameHost, and the pre-patch code accepted it because it was non-null and (given a normally signed-in Glic user) IsAccountConsistent passed, with no IsInPrimaryMainFrame() requirement.
  4. Unauthorized invocation The Glic API action executed on behalf of a subframe that is not the primary Glic surface, allowing a non-top-level context to invoke the privileged API (inferred as to the concrete downstream effect; the patch demonstrates the authorization gap and its rejection, not a specific data-exfiltration primitive).

Impact Assessment

The flaw allowed a *.google.com subframe embedded within an arbitrary non-connectable page to invoke the privileged glicPrivate API, which should be reachable only from the primary main frame — an incorrect-authorization issue in the Extensions component. The advisory classifies it as an AuthorizationError (“Incorrect authorization in Extensions”) and does not report exploitation in the wild. Preconditions include a signed-in Glic-eligible user (so IsAccountConsistent passes) and the relevant Glic feature flags enabled; further escalation beyond unauthorized API invocation is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/glic_private/glic_private_api.cc
modified
GlicSubframeInvokeBrowserTest
chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
modified
GlicSubframeInvokeBrowserTest
chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
modified
if
chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
modified

Files Changed

  • chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
  • chrome/browser/extensions/api/glic_private/glic_private_api.cc

Audit Directions

  • Frame-scoped connectable + top-level assumptions
    Audit extension APIs reachable via externally_connectable for implicit assumptions that the caller is a top-level frame, since subframes at connectable origins can reach them.
  • document_id trust
    Review all GetRfhForDocumentId callers to ensure the resolved frame is validated for frame type (primary main frame vs subframe/prerender/fenced) in addition to existence and account consistency.
  • Centralized vs scattered checks
    Look for privileged APIs that repeat the same validation inline at multiple call sites, where one site may omit a check; consolidating them (as ValidateRenderFrameHost does) prevents divergent gaps.
From 7955fc07e9eee837c60349240875b64e0270ea99 Mon Sep 17 00:00:00 2001
From: Yuheng Huang <yuhengh@chromium.org>
Date: Tue, 07 Jul 2026 18:29:23 -0700
Subject: [PATCH] Only allow primrary main frame to call glic API

Limit glic API to be only callable in primary main frame.

Bug: 513172858
Change-Id: If2575ee181b6d7b697c5e098fbddac6b4baff179
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8028151
Reviewed-by: Justin DeWitt <dewittj@chromium.org>
Commit-Queue: Yuheng Huang <yuhengh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1658433}
---
diff --git a/chrome/browser/extensions/api/glic_private/glic_private_api.cc b/chrome/browser/extensions/api/glic_private/glic_private_api.cc
index 83d39bd..6651ce6 100644
--- a/chrome/browser/extensions/api/glic_private/glic_private_api.cc
+++ b/chrome/browser/extensions/api/glic_private/glic_private_api.cc
@@ -358,6 +358,19 @@
       document_id);
 }
 
+std::optional<extensions::api::glic_private::ErrorCode> ValidateRenderFrameHost(
+    Profile* profile,
+    content::RenderFrameHost* rfh) {
+  if (!rfh || !rfh->IsInPrimaryMainFrame()) {
+    return api::glic_private::ErrorCode::kLocalInvalidDocumentId;
+  }
+  if (!IsAccountConsistent(IdentityManagerFactory::GetForProfile(profile),
+                           *rfh)) {
+    return api::glic_private::ErrorCode::kLocalAccountMismatch;
+  }
+  return std::nullopt;
+}
+
 }  // namespace
 
 GlicPrivateFunction::GlicPrivateFunction() = default;
@@ -385,15 +398,10 @@
   Profile* profile = Profile::FromBrowserContext(browser_context());
 
   content::RenderFrameHost* rfh = GetRfhForDocumentId(params->document_id);
-  if (!rfh) {
-    return RespondNow(Error(api::glic_private::ToString(
-        api::glic_private::ErrorCode::kLocalInvalidDocumentId)));
-  }
-
-  if (!IsAccountConsistent(IdentityManagerFactory::GetForProfile(profile),
-                           *rfh)) {
-    return RespondNow(Error(api::glic_private::ToString(
-        api::glic_private::ErrorCode::kLocalAccountMismatch)));
+  std::optional<extensions::api::glic_private::ErrorCode> validation_error =
+      ValidateRenderFrameHost(profile, rfh);
+  if (validation_error.has_value()) {
+    return RespondNow(Error(api::glic_private::ToString(*validation_error)));
   }
 
   api::glic_private::InvocationSource invocation_source =
@@ -463,15 +471,10 @@
   }
   content::RenderFrameHost* rfh =
       GetRfhForDocumentId(params->details.document_id);
-  if (!rfh) {
-    return RespondNow(GetPromptResponseValueAndLog(
-        api::glic_private::ErrorCode::kLocalInvalidDocumentId));
-  }
-
-  if (!IsAccountConsistent(IdentityManagerFactory::GetForProfile(profile),
-                           *rfh)) {
-    return RespondNow(GetPromptResponseValueAndLog(
-        api::glic_private::ErrorCode::kLocalAccountMismatch));
+  std::optional<extensions::api::glic_private::ErrorCode> validation_error =
+      ValidateRenderFrameHost(profile, rfh);
+  if (validation_error.has_value()) {
+    return RespondNow(GetPromptResponseValueAndLog(*validation_error));
   }
 
   glic::mojom::InvocationSource source =
@@ -571,16 +574,10 @@
   tabs::TabInterface* tab_interface = nullptr;
 
   content::RenderFrameHost* rfh = GetRfhForDocumentId(document_id);
-  if (!rfh) {
-    Respond(GetPromptResponseValueAndLog(
-        api::glic_private::ErrorCode::kLocalInvalidDocumentId));
-    return;
-  }
-
-  if (!IsAccountConsistent(IdentityManagerFactory::GetForProfile(profile),
-                           *rfh)) {
-    Respond(GetPromptResponseValueAndLog(
-        api::glic_private::ErrorCode::kLocalAccountMismatch));
+  std::optional<extensions::api::glic_private::ErrorCode> validation_error =
+      ValidateRenderFrameHost(profile, rfh);
+  if (validation_error.has_value()) {
+    Respond(GetPromptResponseValueAndLog(*validation_error));
     return;
   }
 
diff --git a/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc b/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
index 5a65407..a922c98 100644
--- a/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
+++ b/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
@@ -31,6 +31,7 @@
 #include "components/guest_view/browser/test_guest_view_manager.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/url_loader_interceptor.h"
@@ -788,6 +789,146 @@
       0, std::move(detached_tab), AddTabTypes::ADD_NONE);
 }
 
+// -----------------------------------------------------------------------------
+// Verify that glicPrivate.invoke requires the calling frame to be the primary
+// main frame.
+//
+// If a connectable origin (e.g. *.google.com) subframe is embedded inside a
+// non-connectable top-level frame (e.g. example.com), the subframe can reach
+// the Glic component extension via frame-scoped externally_connectable.
+// However, calling glicPrivate.invoke from the subframe must be rejected by the
+// browser because it is not a primary main frame.
+// -----------------------------------------------------------------------------
+class GlicSubframeInvokeBrowserTest : public GlicPrivateApiTestBase {
+ public:
+  GlicSubframeInvokeBrowserTest() {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {{extensions_features::kApiGlicPrivate, {}},
+         {extensions_features::kApiGlicAccessFromGoogleWebpage, {}},
+         {features::kGlicActor,
+          {{"glic_actor_policy_control_exemption", "true"}}}},
+        {});
+  }
+
+  void SetUpBrowserContextKeyedServices(
+      content::BrowserContext* context) override {
+    GlicPrivateApiTestBase::SetUpBrowserContextKeyedServices(context);
+    // Bind the SigninClient to our TestURLLoaderFactory so that
+    // SetCookieAccounts() can intercept the ListAccounts request.
+    ChromeSigninClientFactory::GetInstance()->SetTestingFactory(
+        context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+                                     &test_url_loader_factory_));
+  }
+
+  void SetUpOnMainThread() override {
+    GlicPrivateApiTestBase::SetUpOnMainThread();
+    // GlicTestEnvironment has already signed in a primary account with the
+    // Glic capability. Additionally:
+    //   - Put that account in the Gaia cookie jar so that
+    //     IsAccountConsistent() passes for *.google.com URLs (this is the
+    //     normal state for a signed-in Glic user; it is a precondition, not a
+    //     security gate against this bug).
+    //   - Enable automatic OAuth token issuance so the prompt EndpointFetcher
+    //     completes (production users have a real refresh token).
+    auto* identity_manager = IdentityManagerFactory::GetForProfile(profile());
+    CoreAccountInfo primary =
+        identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+    ASSERT_FALSE(primary.IsEmpty());
+    identity_adaptor_ =
+        std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+    identity_adaptor_->identity_test_env()->SetTestURLLoaderFactory(
+        &test_url_loader_factory_);
+    identity_adaptor_->identity_test_env()->SetCookieAccounts(
+        {{primary.email, primary.gaia}});
+    identity_adaptor_->identity_test_env()->SetAutomaticIssueOfAccessTokens(
+        true);
+  }
+
+  void TearDownOnMainThread() override {
+    identity_adaptor_.reset();
+    GlicPrivateApiTestBase::TearDownOnMainThread();
+  }
+
+ private:
+  glic::GlicTestEnvironment glic_test_environment_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+  network::TestURLLoaderFactory test_url_loader_factory_;
+  std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> identity_adaptor_;
+};
+
+IN_PROC_BROWSER_TEST_F(GlicSubframeInvokeBrowserTest,
+                       SubframeInNonConnectableOuterTab) {
+  // The Glic component extension must be loaded (kApiGlicPrivate enabled).
+  const Extension* extension =
+      ExtensionRegistry::Get(profile())->enabled_extensions().GetByID(
+          extension_misc::kGlicExtensionId);
+  ASSERT_TRUE(extension);
+
+  // Intercept the prompt-fetch endpoint so OnPromptRetrieved runs to
+  // completion (this is a server response, not a security check; in
+  // production a Glic-eligible signed-in user would receive a real prompt).
+  auto interceptor = CreateMockPromptResponseInterceptor("test prompt");
+
+  // 1) Top-level navigation to an origin that is not in
+  //    externally_connectable.matches (example.com).
+  content::WebContents* tab = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(tab, GURL("https://example.com/iframe.html")));
+  content::RenderFrameHost* main_frame = tab->GetPrimaryMainFrame();
+  ASSERT_EQ("example.com", main_frame->GetLastCommittedURL().host());
+
+  // The top-level main frame CANNOT reach the Glic extension.
+  EXPECT_EQ("no_runtime",
+            content::EvalJs(main_frame,
+                            "(chrome.runtime && chrome.runtime.sendMessage) "
+                            "? 'has_runtime' : 'no_runtime'"));
+
+  // 2) Embed a *.google.com SUBFRAME inside the top-level tab.
+  ASSERT_TRUE(content::NavigateIframeToURL(
+      tab, "test", GURL("https://gemini.google.com/empty.html")));
+  content::RenderFrameHost* subframe = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(subframe);
+  ASSERT_EQ("gemini.google.com", subframe->GetLastCommittedURL().host());
+  ASSERT_FALSE(subframe->IsInPrimaryMainFrame());
+
+  // The *.google.com SUBFRAME *can* reach the Glic extension
+  // (externally_connectable is frame-scoped, not top-level-scoped).
+  ASSERT_EQ("has_runtime",
+            content::EvalJs(subframe,
+                            "(chrome.runtime && chrome.runtime.sendMessage) "
+                            "? 'has_runtime' : 'no_runtime'"));
+
+  // 3) From the SUBFRAME, send glicPrivate.invoke to the component extension.
+  //    background.ts forwards sender.documentId (the SUBFRAME's document) to
+  //    chrome.glicPrivate.invoke.
+  std::string invoke_script = base::StringPrintf(
+      R"(
+      new Promise((resolve) => {
+        chrome.runtime.sendMessage(
+            '%s',
+            {type: 'glicPrivate.invoke',
+             args: {promptId: 'p1', invocationSource: 'universal-cart'}},
+            (response) => {
+              if (chrome.runtime.lastError) {
+                resolve('error: ' + chrome.runtime.lastError.message);
+              } else {
+                resolve('success');
+              }
+            });
+      })
+      )",
+      extension_misc::kGlicExtensionId);
+
+  std::string result = content::EvalJs(subframe, invoke_script).ExtractString();
+
+  // The browser rejects the request because the calling document is not the
+  // tab's primary main frame.
+  EXPECT_EQ("error: Uncaught Error: local-invalid-document-id", result);
+
+  // Verify that the top-level frame remains example.com.
+  EXPECT_EQ("example.com",
+            tab->GetPrimaryMainFrame()->GetLastCommittedURL().host());
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace extensions
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc b/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
index 5a65407..a922c98 100644
--- a/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
+++ b/chrome/browser/extensions/api/glic_private/glic_messaging_browsertest.cc
@@ -31,6 +31,7 @@
 #include "components/guest_view/browser/test_guest_view_manager.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/url_loader_interceptor.h"
@@ -788,6 +789,146 @@
       0, std::move(detached_tab), AddTabTypes::ADD_NONE);
 }
 
+// -----------------------------------------------------------------------------
+// Verify that glicPrivate.invoke requires the calling frame to be the primary
+// main frame.
+//
+// If a connectable origin (e.g. *.google.com) subframe is embedded inside a
+// non-connectable top-level frame (e.g. example.com), the subframe can reach
+// the Glic component extension via frame-scoped externally_connectable.
+// However, calling glicPrivate.invoke from the subframe must be rejected by the
+// browser because it is not a primary main frame.
+// -----------------------------------------------------------------------------
+class GlicSubframeInvokeBrowserTest : public GlicPrivateApiTestBase {
+ public:
+  GlicSubframeInvokeBrowserTest() {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {{extensions_features::kApiGlicPrivate, {}},
+         {extensions_features::kApiGlicAccessFromGoogleWebpage, {}},
+         {features::kGlicActor,
+          {{"glic_actor_policy_control_exemption", "true"}}}},
+        {});
+  }
+
+  void SetUpBrowserContextKeyedServices(
+      content::BrowserContext* context) override {
+    GlicPrivateApiTestBase::SetUpBrowserContextKeyedServices(context);
+    // Bind the SigninClient to our TestURLLoaderFactory so that
+    // SetCookieAccounts() can intercept the ListAccounts request.
+    ChromeSigninClientFactory::GetInstance()->SetTestingFactory(
+        context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader,
+                                     &test_url_loader_factory_));
+  }
+
+  void SetUpOnMainThread() override {
+    GlicPrivateApiTestBase::SetUpOnMainThread();
+    // GlicTestEnvironment has already signed in a primary account with the
+    // Glic capability. Additionally:
+    //   - Put that account in the Gaia cookie jar so that
+    //     IsAccountConsistent() passes for *.google.com URLs (this is the
+    //     normal state for a signed-in Glic user; it is a precondition, not a
+    //     security gate against this bug).
+    //   - Enable automatic OAuth token issuance so the prompt EndpointFetcher
+    //     completes (production users have a real refresh token).
+    auto* identity_manager = IdentityManagerFactory::GetForProfile(profile());
+    CoreAccountInfo primary =
+        identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
+    ASSERT_FALSE(primary.IsEmpty());
+    identity_adaptor_ =
+        std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile());
+    identity_adaptor_->identity_test_env()->SetTestURLLoaderFactory(
+        &test_url_loader_factory_);
+    identity_adaptor_->identity_test_env()->SetCookieAccounts(
+        {{primary.email, primary.gaia}});
+    identity_adaptor_->identity_test_env()->SetAutomaticIssueOfAccessTokens(
+        true);
+  }
+
+  void TearDownOnMainThread() override {
+    identity_adaptor_.reset();
+    GlicPrivateApiTestBase::TearDownOnMainThread();
+  }
+
+ private:
+  glic::GlicTestEnvironment glic_test_environment_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+  network::TestURLLoaderFactory test_url_loader_factory_;
+  std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> identity_adaptor_;
+};
+
+IN_PROC_BROWSER_TEST_F(GlicSubframeInvokeBrowserTest,
+                       SubframeInNonConnectableOuterTab) {
+  // The Glic component extension must be loaded (kApiGlicPrivate enabled).
+  const Extension* extension =
+      ExtensionRegistry::Get(profile())->enabled_extensions().GetByID(
+          extension_misc::kGlicExtensionId);
+  ASSERT_TRUE(extension);
+
+  // Intercept the prompt-fetch endpoint so OnPromptRetrieved runs to
+  // completion (this is a server response, not a security check; in
+  // production a Glic-eligible signed-in user would receive a real prompt).
+  auto interceptor = CreateMockPromptResponseInterceptor("test prompt");
+
+  // 1) Top-level navigation to an origin that is not in
+  //    externally_connectable.matches (example.com).
+  content::WebContents* tab = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(tab, GURL("https://example.com/iframe.html")));
+  content::RenderFrameHost* main_frame = tab->GetPrimaryMainFrame();
+  ASSERT_EQ("example.com", main_frame->GetLastCommittedURL().host());
+
+  // The top-level main frame CANNOT reach the Glic extension.
+  EXPECT_EQ("no_runtime",
+            content::EvalJs(main_frame,
+                            "(chrome.runtime && chrome.runtime.sendMessage) "
+                            "? 'has_runtime' : 'no_runtime'"));
+
+  // 2) Embed a *.google.com SUBFRAME inside the top-level tab.
+  ASSERT_TRUE(content::NavigateIframeToURL(
+      tab, "test", GURL("https://gemini.google.com/empty.html")));
+  content::RenderFrameHost* subframe = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(subframe);
+  ASSERT_EQ("gemini.google.com", subframe->GetLastCommittedURL().host());
+  ASSERT_FALSE(subframe->IsInPrimaryMainFrame());
+
+  // The *.google.com SUBFRAME *can* reach the Glic extension
+  // (externally_connectable is frame-scoped, not top-level-scoped).
+  ASSERT_EQ("has_runtime",
+            content::EvalJs(subframe,
+                            "(chrome.runtime && chrome.runtime.sendMessage) "
+                            "? 'has_runtime' : 'no_runtime'"));
+
+  // 3) From the SUBFRAME, send glicPrivate.invoke to the component extension.
+  //    background.ts forwards sender.documentId (the SUBFRAME's document) to
+  //    chrome.glicPrivate.invoke.
+  std::string invoke_script = base::StringPrintf(
+      R"(
+      new Promise((resolve) => {
+        chrome.runtime.sendMessage(
+            '%s',
+            {type: 'glicPrivate.invoke',
+             args: {promptId: 'p1', invocationSource: 'universal-cart'}},
+            (response) => {
+              if (chrome.runtime.lastError) {
+                resolve('error: ' + chrome.runtime.lastError.message);
+              } else {
+                resolve('success');
+              }
+            });
+      })
+      )",
+      extension_misc::kGlicExtensionId);
+
+  std::string result = content::EvalJs(subframe, invoke_script).ExtractString();
+
+  // The browser rejects the request because the calling document is not the
+  // tab's primary main frame.
+  EXPECT_EQ("error: Uncaught Error: local-invalid-document-id", result);
+
+  // Verify that the top-level frame remains example.com.
+  EXPECT_EQ("example.com",
+            tab->GetPrimaryMainFrame()->GetLastCommittedURL().host());
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace extensions
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.