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 <[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.
References
On This Page