High CVSS 8.8 chrome Input Validation 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Glic
DescriptionInsufficient validation of untrusted input in Glic
ComponentGlic
Sub-componentGlic
Bug ClassInput Validation
Tracker511739631
Fix commit6b6566b38403 (chromium/src) +206/-22
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-10 (per vendor advisory)
Fix landed2026-05-21 (public git)
Disclosed2026-06-30
Patch gap40 days — fix public in source before the advisory

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.