High chrome Logic Error 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Glic
DescriptionInsufficient validation of untrusted input in Glic
ComponentGlic
Bug ClassLogic Error
Tracker511739631
Fix commit6b6566b38403 (chromium/src) +206/-22
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
GlicWebUiBrowserTest
chrome/browser/glic/host/glic_webui_browsertest.cc
modified
GlicWebUiBrowserTest
chrome/browser/glic/host/glic_webui_browsertest.cc
modified
RunUntil
chrome/browser/glic/host/glic_webui_browsertest.cc
modified
if
chrome/browser/resources/glic/webview.ts
modified

Files Changed

  • chrome/browser/glic/BUILD.gn
  • chrome/browser/glic/host/glic_ui.cc
  • chrome/browser/glic/host/glic_webui_browsertest.cc
  • chrome/browser/resources/glic/glic.ts
  • chrome/browser/resources/glic/webview.ts
From 6b6566b38403eff36d6aff72112dcef21757bb93 Mon Sep 17 00:00:00 2001
From: Dan Harrington <[email protected]>
Date: Thu, 21 May 2026 12:59:53 -0700
Subject: [PATCH] glic: Restrict Mojo API access to authorized guest origins.

Validate guest origins before setting up GlicApiHost and binding privileged WebClientHandler Mojo interface, preventing privilege escalation from untrusted origins.
- Added kGlicApiAllowedOrigins feature parameter to configure authorized origins.
- Passed list to WebUI and wrapped host/communicator instantiation in origin checks.
- Allowed untrusted navigation-allowed domains (e.g., captchas) to render without Mojo privileges.
- Added test in glic_webui_browsertest.cc.

Bug: b:511739631
Link: https://chromium-review.googlesource.com/id/If59f12f34f1bbe5f3f6c9ede09d9af7c6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859248
Commit-Queue: Dan H <[email protected]>
Reviewed-by: Justin DeWitt <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1634482}
---

diff --git a/chrome/browser/glic/BUILD.gn b/chrome/browser/glic/BUILD.gn
index e4983a35..8db266e 100644
--- a/chrome/browser/glic/BUILD.gn
+++ b/chrome/browser/glic/BUILD.gn
@@ -769,6 +769,7 @@
       "host/context/glic_tab_data_browsertest.cc",
       "host/glic_api_browsertest.cc",
       "host/glic_permission_enforcement_browsertest.cc",
+      "host/glic_webui_browsertest.cc",
       "host/guest_util_browsertest.cc",
       "public/glic_enabling_browsertest.cc",
       "widget/glic_side_panel_coordinator_browsertest.cc",
diff --git a/chrome/browser/glic/host/glic_ui.cc b/chrome/browser/glic/host/glic_ui.cc
index 3c6fbbc4c2..168a684 100644
--- a/chrome/browser/glic/host/glic_ui.cc
+++ b/chrome/browser/glic/host/glic_ui.cc
@@ -302,6 +302,8 @@
   }
 
   source->AddString("glicAllowedOrigins", allowed_origins);
+  source->AddString("glicApiAllowedOrigins",
+                    features::kGlicApiAllowedOrigins.Get());
 
   bool reload_after_navigation =
       !command_line->HasSwitch(::switches::kGlicSkipReloadAfterNavigation);
diff --git a/chrome/browser/glic/host/glic_webui_browsertest.cc b/chrome/browser/glic/host/glic_webui_browsertest.cc
new file mode 100644
index 0000000..4711a57
--- /dev/null
+++ b/chrome/browser/glic/host/glic_webui_browsertest.cc
@@ -0,0 +1,76 @@
+// 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 "base/test/scoped_feature_list.h"
+#include "chrome/browser/glic/glic_pref_names.h"
+#include "chrome/browser/glic/host/guest_util.h"
+#include "chrome/browser/glic/host/host.h"
+#include "chrome/browser/glic/public/glic_keyed_service.h"
+#include "chrome/browser/glic/test_support/glic_browser_test.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/dns/mock_host_resolver.h"
+
+namespace glic {
+
+class GlicWebUiBrowserTest : public glic::GlicBrowserTest {
+ public:
+  GlicWebUiBrowserTest() {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {
+            {features::kGlicCSPConfig, {}},
+        },
+        {});
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    glic::GlicBrowserTest::SetUpCommandLine(command_line);
+    // Allow b.com to load in the webview, but not have Glic API access
+    command_line->AppendSwitchASCII(::switches::kGlicAllowedOrigins,
+                                    "https://gemini.google.com http://b.com");
+    command_line->AppendSwitch(::switches::kGlicSkipReloadAfterNavigation);
+  }
+
+  void SetUpOnMainThread() override {
+    glic::GlicBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("b.com", "127.0.0.1");
+    SetFRECompletion(GetProfile(), prefs::FreStatus::kCompleted);
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(GlicWebUiBrowserTest,
+                       NavigatingToUntrustedOriginRevokesApi) {
+  // 1. Open Glic on the primary guest URL (which is API allowed)
+  ASSERT_OK_AND_ASSIGN(auto* instance, OpenGlicForActiveTab());
+  ASSERT_TRUE(WaitForGlicClient(instance).has_value());
+  EXPECT_TRUE(instance->host().IsWebClientConnected());
+
+  // 2. Obtain the guest WebContents and trigger navigation to b.com (untrusted)
+  content::WebContents* guest_contents =
+      GetGlicGuestWebContents(instance->host().webui_contents());
+  ASSERT_TRUE(guest_contents);
+
+  GURL untrusted_guest_url = embedded_test_server()->GetURL(
+      "b.com", "/glic/browser_tests/minimal_client.html");
+  ASSERT_TRUE(content::NavigateToURL(guest_contents, untrusted_guest_url));
+
+  // 3. Verify Glic API/Mojo connection is immediately revoked
+  EXPECT_TRUE(
+      RunUntil([&]() { return !instance->host().IsWebClientConnected(); },
+               "Wait for Glic WebClient to disconnect"));
+
+  // 4. Ensure Glic API remains disconnected
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(500));
+  run_loop.Run();
+  EXPECT_FALSE(instance->host().IsWebClientConnected());
+}
+
+}  // namespace glic
diff --git a/chrome/browser/resources/glic/glic.ts b/chrome/browser/resources/glic/glic.ts
index 960bb75c..21dd2cc 100644
--- a/chrome/browser/resources/glic/glic.ts
+++ b/chrome/browser/resources/glic/glic.ts
@@ -17,4 +17,4 @@
 export {PostMessageRequestSender, PostMessageRouter, Queue} from './glic_api_impl/post_message_transport.js';
 export {ObservableValue, Subject} from './observable.js';
 export type {PageType, WebviewDelegate} from './webview.js';
-export {matcherForOrigin, urlMatchesAllowedOrigin, WebviewController, WebviewPersistentState} from './webview.js';
+export {matcherForOrigin, urlMatchesAllowedOrigin, urlMatchesApiAllowedOrigin, WebviewController, WebviewPersistentState} from './webview.js';
diff --git a/chrome/browser/resources/glic/webview.ts b/chrome/browser/resources/glic/webview.ts
index af43d6c..5f110b3 100644
--- a/chrome/browser/resources/glic/webview.ts
+++ b/chrome/browser/resources/glic/webview.ts
@@ -445,10 +445,11 @@
 
     this.destroyHost(WebClientState.UNINITIALIZED);
 
-    const origin = new URL(url).origin;
-    if (this.webview.contentWindow && origin !== 'null') {
+    const urlObj = URL.parse(url);
+    if (urlObj && this.webview.contentWindow &&
+        urlMatchesApiAllowedOrigin(urlObj)) {
       this.communicator =
-          new GlicApiCommunicator(origin, this.webview.contentWindow);
+          new GlicApiCommunicator(urlObj.origin, this.webview.contentWindow);
       this.host = new GlicApiHost(
           this.browserProxy, this.communicator, this.hostEmbedder);
       this.hostSubscriber = this.host.getWebClientState().subscribe(state => {
@@ -474,7 +475,7 @@
       return;
     }
 
-    if (new URL(url).pathname.startsWith('/sorry/')) {
+    if (urlObj?.pathname.startsWith('/sorry/')) {
       this.delegate.webviewPageCommit('guestError');
       return;
     }
@@ -534,7 +535,7 @@
               return {cancel: true};
             }
 
-            return {cancel: !urlMatchesAllowedOrigin(details.url)};
+            return {cancel: !urlMatchesAllowedOrigin(new URL(details.url))};
           };
 }
 
@@ -570,20 +571,15 @@
   allowedOrigins.push(...loadTimeData.getString('glicAllowedOrigins')
                           .split(' ')
                           .map(origin => origin.trim()));
+  allowedOrigins.push(...loadTimeData.getString('glicApiAllowedOrigins')
+                          .split(' ')
+                          .map(origin => origin.trim()));
   return new OriginCheckParams([ResourceType.MAIN_FRAME], allowedOrigins);
 }
 // </if>
 
-export function urlMatchesAllowedOrigin(url: string) {
-  // For development.
-  if (loadTimeData.getBoolean('devMode')) {
-    return true;
-  }
-
-  // A URL is allowed if it either matches glicGuestURL's origin, or it matches
-  // any of the approved origins.
-  const defaultUrl = new URL(loadTimeData.getString('glicGuestURL'));
-  if (matcherForOrigin(defaultUrl.origin)?.test(url)) {
+export function urlMatchesAllowedOrigin(url: URL) {
+  if (urlMatchesApiAllowedOrigin(url)) {
     return true;
   }
 
@@ -591,3 +587,29 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/glic/host/glic_webui_browsertest.cc b/chrome/browser/glic/host/glic_webui_browsertest.cc
new file mode 100644
index 0000000..4711a57
--- /dev/null
+++ b/chrome/browser/glic/host/glic_webui_browsertest.cc
@@ -0,0 +1,76 @@
+// 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 "base/test/scoped_feature_list.h"
+#include "chrome/browser/glic/glic_pref_names.h"
+#include "chrome/browser/glic/host/guest_util.h"
+#include "chrome/browser/glic/host/host.h"
+#include "chrome/browser/glic/public/glic_keyed_service.h"
+#include "chrome/browser/glic/test_support/glic_browser_test.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/common/chrome_switches.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/browser_test_utils.h"
+#include "net/dns/mock_host_resolver.h"
+
+namespace glic {
+
+class GlicWebUiBrowserTest : public glic::GlicBrowserTest {
+ public:
+  GlicWebUiBrowserTest() {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {
+            {features::kGlicCSPConfig, {}},
+        },
+        {});
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    glic::GlicBrowserTest::SetUpCommandLine(command_line);
+    // Allow b.com to load in the webview, but not have Glic API access
+    command_line->AppendSwitchASCII(::switches::kGlicAllowedOrigins,
+                                    "https://gemini.google.com http://b.com");
+    command_line->AppendSwitch(::switches::kGlicSkipReloadAfterNavigation);
+  }
+
+  void SetUpOnMainThread() override {
+    glic::GlicBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("b.com", "127.0.0.1");
+    SetFRECompletion(GetProfile(), prefs::FreStatus::kCompleted);
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(GlicWebUiBrowserTest,
+                       NavigatingToUntrustedOriginRevokesApi) {
+  // 1. Open Glic on the primary guest URL (which is API allowed)
+  ASSERT_OK_AND_ASSIGN(auto* instance, OpenGlicForActiveTab());
+  ASSERT_TRUE(WaitForGlicClient(instance).has_value());
+  EXPECT_TRUE(instance->host().IsWebClientConnected());
+
+  // 2. Obtain the guest WebContents and trigger navigation to b.com (untrusted)
+  content::WebContents* guest_contents =
+      GetGlicGuestWebContents(instance->host().webui_contents());
+  ASSERT_TRUE(guest_contents);
+
+  GURL untrusted_guest_url = embedded_test_server()->GetURL(
+      "b.com", "/glic/browser_tests/minimal_client.html");
+  ASSERT_TRUE(content::NavigateToURL(guest_contents, untrusted_guest_url));
+
+  // 3. Verify Glic API/Mojo connection is immediately revoked
+  EXPECT_TRUE(
+      RunUntil([&]() { return !instance->host().IsWebClientConnected(); },
+               "Wait for Glic WebClient to disconnect"));
+
+  // 4. Ensure Glic API remains disconnected
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(500));
+  run_loop.Run();
+  EXPECT_FALSE(instance->host().IsWebClientConnected());
+}
+
+}  // namespace glic
diff --git a/chrome/test/data/webui/glic/unit_tests/test_helpers.ts b/chrome/test/data/webui/glic/unit_tests/test_helpers.ts
index ed802a6..b51aef0 100644
--- a/chrome/test/data/webui/glic/unit_tests/test_helpers.ts
+++ b/chrome/test/data/webui/glic/unit_tests/test_helpers.ts
@@ -13,6 +13,7 @@
   loadTimeData.resetForTesting(Object.assign(
       {
         glicAllowedOrigins: '',
+        glicApiAllowedOrigins: '',
         glicGuestURL: 'https://cat.fun/',
         devMode: false,
         chromeVersion: '123.0.0.0',
diff --git a/chrome/test/data/webui/glic/unit_tests/webview_test.ts b/chrome/test/data/webui/glic/unit_tests/webview_test.ts
index dc75961f..7935b3ce 100644
--- a/chrome/test/data/webui/glic/unit_tests/webview_test.ts
+++ b/chrome/test/data/webui/glic/unit_tests/webview_test.ts
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {matcherForOrigin, urlMatchesAllowedOrigin, WebviewController, WebviewPersistentState, ZoomAction} from 'chrome://glic/glic.js';
+import {matcherForOrigin, urlMatchesAllowedOrigin, urlMatchesApiAllowedOrigin, WebviewController, WebviewPersistentState, ZoomAction} from 'chrome://glic/glic.js';
 import type {CrA11yAnnouncerMessagesSentEvent} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
@@ -16,14 +16,15 @@
 
 import {configureLoadTimeData, FakeApiHostEmbedder, FakeBrowserProxy, FakeWebviewDelegate} from './test_helpers.js';
 
-suite('WebviewTest', () => {
+suite('urlMatchesAllowedOriginTest', () => {
   setup(() => {
     configureLoadTimeData();
   });
 
   function assertUrlMatchesAllowedOrigin(expectMatches: boolean, url: string) {
+    const urlObj = new URL(url);
     assertEquals(
-        expectMatches, urlMatchesAllowedOrigin(url),
+        expectMatches, urlMatchesAllowedOrigin(urlObj),
         `urlMatchesAllowedOrigin("${url}")`);
   }
 
@@ -60,7 +61,7 @@
     assertEquals('http', result?.protocol);
   });
 
-  test('urlMatchesAllowedOrigin allows the primary url', () => {
+  test('allows the primary url', () => {
     loadTimeData.overrideValues({
       glicAllowedOrigins: '',
       glicGuestURL: 'https://cat.fun/party',
@@ -72,7 +73,7 @@
     assertUrlMatchesAllowedOrigin(false, 'http://cat.fun/');
   });
 
-  test('urlMatchesAllowedOrigin allows allowed origins', () => {
+  test('allows allowed origins', () => {
     loadTimeData.overrideValues({
       glicAllowedOrigins: 'https://*.mouse.org https://dog.com',
       glicGuestURL: 'https://cat.fun/party',
@@ -88,7 +89,17 @@
     assertUrlMatchesAllowedOrigin(false, 'http://dog.com/party');
   });
 
-  test('urlMatchesAllowedOrigin allows http', () => {
+  test('allows api allowed origins', () => {
+    loadTimeData.overrideValues({
+      glicAllowedOrigins: 'https://dog.com',
+      glicApiAllowedOrigins: 'https://*.mouse.org',
+      glicGuestURL: 'https://cat.fun/party',
+    });
+
+    assertUrlMatchesAllowedOrigin(true, 'https://sub.mouse.org/party');
+  });
+
+  test('allows http', () => {
     loadTimeData.overrideValues({
       glicAllowedOrigins: '',
       glicGuestURL: 'http://test.com',
@@ -100,6 +111,66 @@
   });
 });
 
+suite('urlMatchesApiAllowedOriginTest', () => {
+  setup(() => {
+    configureLoadTimeData();
+  });
+
+  function assertUrlMatchesApiAllowedOrigin(
+      expectMatches: boolean, url: string) {
+    assertEquals(
+        expectMatches, urlMatchesApiAllowedOrigin(new URL(url)),
+        `urlMatchesApiAllowedOrigin("${url}")`);
+  }
+
+  test('allows guest origin', () => {
+    loadTimeData.overrideValues({
+      glicGuestURL: 'https://cat.fun/party',
+      glicApiAllowedOrigins: '',
+      devMode: false,
+    });
+    assertUrlMatchesApiAllowedOrigin(true, 'https://cat.fun/party');
+    assertUrlMatchesApiAllowedOrigin(true, 'https://cat.fun/disaster');
+    assertUrlMatchesApiAllowedOrigin(true, 'https://cat.fun/');
+    assertUrlMatchesApiAllowedOrigin(false, 'https://dog.fun/');
+  });
+
+  test('allows api allowed origins', () => {
+    loadTimeData.overrideValues({
+      glicGuestURL: 'https://cat.fun/party',
+      glicApiAllowedOrigins: 'https://*.mouse.org https://dog.com',
+      devMode: false,
+    });
+    assertUrlMatchesApiAllowedOrigin(true, 'https://sub.mouse.org/party');
+    assertUrlMatchesApiAllowedOrigin(true, 'https://inner.sub.mouse.org/party');
+    assertUrlMatchesApiAllowedOrigin(false, 'https://mouse.org');
+    assertUrlMatchesApiAllowedOrigin(true, 'https://dog.com/party');
+    assertUrlMatchesApiAllowedOrigin(false, 'http://dog.com/party');
+  });
+
+  test('devMode bypasses checks', () => {
+    loadTimeData.overrideValues({
+      glicGuestURL: 'https://cat.fun/party',
+      glicApiAllowedOrigins: '',
+      devMode: true,
+    });
+    assertUrlMatchesApiAllowedOrigin(true, 'https://cat.fun/party');
+    assertUrlMatchesApiAllowedOrigin(true, 'https://dog.fun/');
+  });
+
+  test('handles null origin', () => {
+    loadTimeData.overrideValues({
+      glicGuestURL: 'https://cat.fun/party',
+      glicApiAllowedOrigins: 'https://dog.com',
+      devMode: true,
+    });
+    // A URL with 'null' origin should not be allowed even in devMode
+    const nullOriginUrl = new URL('data:text/html,hello');
+    assertFalse(urlMatchesApiAllowedOrigin(nullOriginUrl));
+  });
+});
+
+
 suite('WebviewZoomTest', () => {
   let controller: WebviewController;
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.