Chrome · Glic
CVE-2026-13817
Logic Error in Glic
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
GlicWebUiBrowserTestchrome/browser/glic/host/glic_webui_browsertest.cc |
modified | |
GlicWebUiBrowserTestchrome/browser/glic/host/glic_webui_browsertest.cc |
modified | |
RunUntilchrome/browser/glic/host/glic_webui_browsertest.cc |
modified | |
ifchrome/browser/resources/glic/webview.ts |
modified |
Files Changed
chrome/browser/glic/BUILD.gnchrome/browser/glic/host/glic_ui.ccchrome/browser/glic/host/glic_webui_browsertest.ccchrome/browser/resources/glic/glic.tschrome/browser/resources/glic/webview.ts
Patch
From 6b6566b38403eff36d6aff72112dcef21757bb93 Mon Sep 17 00:00:00 2001
From: Dan Harrington <harringtond@chromium.org>
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 <harringtond@chromium.org>
Reviewed-by: Justin DeWitt <dewittj@chromium.org>
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.
References
On This Page