Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Extensions
DescriptionIncorrect authorization in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker513172858
Fix commit7955fc07e9ee (chromium/src) +166/-28
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
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
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_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
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());
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.