Chrome · Extensions
CVE-2026-106331
Input Validation in Extensions
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/extensions/api/history/history_apitest.cc |
modified | |
TESTchrome/browser/history/history_utils_unittest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc |
modified | |
ifchrome/browser/extensions/api/history/BUILD.gn |
modified | |
source_setchrome/browser/history/BUILD.gn |
modified |
Files Changed
chrome/browser/history/BUILD.gnchrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.ccchrome/browser/ui/web_applications/navigation_capturing_process.ccchrome/browser/history/history_utils_unittest.ccchrome/browser/extensions/api/history/BUILD.gnchrome/browser/extensions/api/history/history_api.ccchrome/browser/history/history_utils.ccchrome/browser/extensions/api/history/history_apitest.cc
Patch
From 286508da4427ff0d111e66105a16860e2e198322 Mon Sep 17 00:00:00 2001 From: Simon Hangl <[email protected]> Date: Sun, 13 Sep 2026 03:12:02 -0700 Subject: [PATCH] Prevent IWA deep-link bypass via extension chrome.history and UI Reject isolated-app:// in the extension chrome.history API, exclude it from history persistence, and cancel PAGE_TRANSITION_AUTO_BOOKMARK navigations to block deep-link navigation bypasses. Doc: https://docs.google.com/document/d/1oLNvnElkcSEh9NieS-tk-3GDJoHvde_XeTrMC87J2vE/edit?usp=sharing&resourcekey=0-sl7j_elotLFQBrCS60_Eig Bug: 553256068 Change-Id: I46ca96e35abe0a91b8fbe8f7756d9c31418b9879 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8366310 Reviewed-by: Moe Ahmadi <[email protected]> Commit-Queue: Simon Hangl <[email protected]> Reviewed-by: Mike Wasserman <[email protected]> Reviewed-by: Reilly Grant <[email protected]> Reviewed-by: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1696954} --- diff --git a/chrome/browser/extensions/api/history/history_api.cc b/chrome/browser/extensions/api/history/history_api.cc index 9a95a92..651742c2 100644 --- a/chrome/browser/extensions/api/history/history_api.cc +++ b/chrome/browser/extensions/api/history/history_api.cc @@ -37,6 +37,10 @@ #include "extensions/buildflags/buildflags.h" #include "ui/base/page_transition_types.h" +#if !BUILDFLAG(IS_ANDROID) +#include "components/webapps/isolated_web_apps/scheme.h" +#endif + static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); namespace extensions { @@ -227,6 +231,16 @@ *error = kInvalidUrlError; return false; } +#if !BUILDFLAG(IS_ANDROID) + // Match tabs.create/tabs.update and bookmarks.create: extensions may not + // plant isolated-app:// deep-link history entries. Opening such an entry + // would deep-link the IWA via PAGE_TRANSITION_AUTO_BOOKMARK, bypassing + // start_url + launchQueue routing. + if (temp_url.SchemeIs(webapps::kIsolatedAppScheme)) { + *error = kInvalidUrlError; + return false; + } +#endif url->Swap(&temp_url); return true; } diff --git a/chrome/browser/history/history_utils.cc b/chrome/browser/history/history_utils.cc index 6da2d4cc..71be100 100644 --- a/chrome/browser/history/history_utils.cc +++ b/chrome/browser/history/history_utils.cc @@ -4,10 +4,15 @@ #include "chrome/browser/history/history_utils.h" +#include "build/build_config.h" #include "chrome/common/url_constants.h" #include "components/dom_distiller/core/url_constants.h" #include "url/gurl.h" +#if !BUILDFLAG(IS_ANDROID) +#include "components/webapps/isolated_web_apps/scheme.h" +#endif + bool CanAddURLToHistory(const GURL& url) { if (!url.is_valid()) return false; @@ -27,5 +32,13 @@ url.SchemeIs(dom_distiller::kDomDistillerScheme)) return false; +#if !BUILDFLAG(IS_ANDROID) + // Isolated Web Apps are isolated applications and must not be registered in + // browsing history. + if (url.SchemeIs(webapps::kIsolatedAppScheme)) { + return false; + } +#endif + return true; } diff --git a/chrome/browser/ui/web_applications/navigation_capturing_process.cc b/chrome/browser/ui/web_applications/navigation_capturing_process.cc index d5b950ca..431653a 100644 --- a/chrome/browser/ui/web_applications/navigation_capturing_process.cc +++ b/chrome/browser/ui/web_applications/navigation_capturing_process.cc @@ -294,14 +294,16 @@ return true; } - // Any links: window.open(), anchor link, meta tag redirect. Cancel + // Any links or browser UI navigations (bookmarks, history): Cancel // navigations that do not originate from a browser belonging to the target // app's family (the app itself, its parent app, or a sibling sub-app), // regardless of disposition, before falling through to the // disposition-specific handling. With sub-apps, navigations are allowed in // all directions as long as both apps are in the same family. - if (ui::PageTransitionCoreTypeIs(params.transition, - ui::PAGE_TRANSITION_LINK) && + if ((ui::PageTransitionCoreTypeIs(params.transition, + ui::PAGE_TRANSITION_LINK) || + ui::PageTransitionCoreTypeIs(params.transition, + ui::PAGE_TRANSITION_AUTO_BOOKMARK)) && (!source_browser_app_id || registrar.GetParentAppId(*source_browser_app_id) .value_or(*source_browser_app_id) != diff --git a/chrome/browser/extensions/api/history/history_apitest.cc b/chrome/browser/extensions/api/history/history_apitest.cc index 1ec671fa..13b2b33 100644 --- a/chrome/browser/extensions/api/history/history_apitest.cc +++ b/chrome/browser/extensions/api/history/history_apitest.cc @@ -415,4 +415,43 @@ "countItemsInHistory()")); } +#if !BUILDFLAG(IS_ANDROID) +IN_PROC_BROWSER_TEST_F(HistoryApiTest, AddUrl_IsolatedAppRejected) { + static constexpr char kManifest[] = + R"({ + "name": "chrome.history", + "version": "0.1", + "manifest_version": 3, + "permissions": ["history"], + "background": { + "service_worker": "add_url_iwa.js" + } + })"; + static constexpr char kBackgroundJs[] = + R"(chrome.test.runTests([ + function addIwaUrl() { + const iwaUrl = 'isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/path'; + chrome.history.addUrl({url: iwaUrl}, function() { + chrome.test.assertLastError('Url is invalid.'); + chrome.history.getVisits({url: iwaUrl}, function() { + chrome.test.assertLastError('Url is invalid.'); + chrome.history.deleteUrl({url: iwaUrl}, function() { + chrome.test.assertLastError('Url is invalid.'); + chrome.history.search({text: ''}, function(results) { + chrome.test.assertEq(0, results.length); + chrome.test.succeed(); + }); + }); + }); + }); + } + ]);)"; + TestExtensionDir test_dir; + test_dir.WriteManifest(kManifest); + test_dir.WriteFile(FILE_PATH_LITERAL("add_url_iwa.js"), kBackgroundJs); + + ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_; +} +#endif + } // namespace extensions diff --git a/chrome/browser/history/history_utils_unittest.cc b/chrome/browser/history/history_utils_unittest.cc new file mode 100644 index 0000000..896c62b --- /dev/null +++ b/chrome/browser/history/history_utils_unittest.cc @@ -0,0 +1,33 @@ +// 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 "chrome/browser/history/history_utils.h" + +#include "build/build_config.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +#if !BUILDFLAG(IS_ANDROID) +#include "components/webapps/isolated_web_apps/scheme.h" +#endif + +TEST(HistoryUtilsTest, CanAddURLToHistory) { + EXPECT_TRUE(CanAddURLToHistory(GURL("https://www.google.com"))); + EXPECT_TRUE(CanAddURLToHistory(GURL("http://example.com/path?query=1"))); + + EXPECT_FALSE(CanAddURLToHistory(GURL())); + EXPECT_FALSE(CanAddURLToHistory(GURL("invalid-url"))); + EXPECT_FALSE(CanAddURLToHistory(GURL("javascript:alert(1)"))); + EXPECT_FALSE(CanAddURLToHistory(GURL("about:blank"))); + EXPECT_FALSE(CanAddURLToHistory(GURL("chrome://version"))); + EXPECT_FALSE(CanAddURLToHistory(GURL("chrome-untrusted://terminal"))); + EXPECT_FALSE(CanAddURLToHistory(GURL("view-source:https://www.google.com"))); + +#if !BUILDFLAG(IS_ANDROID) + EXPECT_FALSE(CanAddURLToHistory( + GURL("isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/index.html"))); + EXPECT_FALSE(CanAddURLToHistory( + GURL("isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/secret/admin"))); +#endif +} diff --git a/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc index 8a2b250..635ee82e 100644 --- a/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc +++ b/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc @@ -216,6 +216,28 @@ browser()->tab_strip_model()->GetWebContentsAt(1)->GetURL()); } +IN_PROC_BROWSER_TEST_F(BrowserNavigatorIwaTest, + AutoBookmarkNavigationCancelled) { + ASSERT_NO_FATAL_FAILURE(InstallBundles()); + + const size_t browsers_before = + GlobalBrowserCollection::GetInstance()->GetSize(); + + // Navigating to an IWA via PAGE_TRANSITION_AUTO_BOOKMARK from a non-app + // context (e.g. History menu, History clusters, bookmarks) must be cancelled + // as a cross-IWA navigation. + NavigateParams params = MakeNavigateParams(browser()); + params.url = url_info1_->origin().GetURL().Resolve("/deep-link.html"); + params.transition = ui::PAGE_TRANSITION_AUTO_BOOKMARK; + params.disposition = WindowOpenDisposition::NEW_WINDOW; + base::WeakPtr<content::NavigationHandle> handle = Navigate(¶ms); + EXPECT_EQ(nullptr, handle); + + EXPECT_EQ(browsers_before, GlobalBrowserCollection::GetInstance()->GetSize()); + EXPECT_FALSE(web_app::AppBrowserController::FindForWebApp( + *profile(), url_info1_->app_id())); +} + #if BUILDFLAG(IS_CHROMEOS) IN_PROC_BROWSER_TEST_F(BrowserNavigatorIwaTest, WindowOpenProtocol) { ASSERT_NO_FATAL_FAILURE(InstallBundles()); diff --git a/chrome/browser/extensions/api/history/BUILD.gn b/chrome/browser/extensions/api/history/BUILD.gn index f7dd014..f7afd61 100644 --- a/chrome/browser/extensions/api/history/BUILD.gn +++ b/chrome/browser/extensions/api/history/BUILD.gn @@ -29,4 +29,8 @@ "//extensions/browser", "//extensions/buildflags", ] + + if (!is_android) { + deps += [ "//components/webapps/isolated_web_apps:scheme" ] + } } diff --git a/chrome/browser/history/BUILD.gn b/chrome/browser/history/BUILD.gn index e0b67f0..6396f62 100644 --- a/chrome/browser/history/BUILD.gn +++ b/chrome/browser/history/BUILD.gn @@ -97,7 +97,10 @@ "//chrome/browser/ui/android/tab_model:impl", ] } else { - deps += [ "//chrome/browser/ui/browser_window" ] + deps += [ + "//chrome/browser/ui/browser_window", + "//components/webapps/isolated_web_apps:scheme", + ] } } @@ -196,7 +199,10 @@ source_set("unit_tests") { testonly = true - sources = [ "history_tab_helper_unittest.cc" ] + sources = [ + "history_tab_helper_unittest.cc", + "history_utils_unittest.cc", + ] deps = [ ":history", "//base", @@ -220,6 +226,8 @@ "//components/feed/core/v2:feed_core_stubs", "//components/feed/core/v2:feed_core_v2", ] + } else { + deps += [ "//components/webapps/isolated_web_apps:scheme" ] } }
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/history/history_apitest.cc b/chrome/browser/extensions/api/history/history_apitest.cc
index 1ec671fa..13b2b33 100644
--- a/chrome/browser/extensions/api/history/history_apitest.cc
+++ b/chrome/browser/extensions/api/history/history_apitest.cc
@@ -415,4 +415,43 @@
"countItemsInHistory()"));
}
+#if !BUILDFLAG(IS_ANDROID)
+IN_PROC_BROWSER_TEST_F(HistoryApiTest, AddUrl_IsolatedAppRejected) {
+ static constexpr char kManifest[] =
+ R"({
+ "name": "chrome.history",
+ "version": "0.1",
+ "manifest_version": 3,
+ "permissions": ["history"],
+ "background": {
+ "service_worker": "add_url_iwa.js"
+ }
+ })";
+ static constexpr char kBackgroundJs[] =
+ R"(chrome.test.runTests([
+ function addIwaUrl() {
+ const iwaUrl = 'isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/path';
+ chrome.history.addUrl({url: iwaUrl}, function() {
+ chrome.test.assertLastError('Url is invalid.');
+ chrome.history.getVisits({url: iwaUrl}, function() {
+ chrome.test.assertLastError('Url is invalid.');
+ chrome.history.deleteUrl({url: iwaUrl}, function() {
+ chrome.test.assertLastError('Url is invalid.');
+ chrome.history.search({text: ''}, function(results) {
+ chrome.test.assertEq(0, results.length);
+ chrome.test.succeed();
+ });
+ });
+ });
+ });
+ }
+ ]);)";
+ TestExtensionDir test_dir;
+ test_dir.WriteManifest(kManifest);
+ test_dir.WriteFile(FILE_PATH_LITERAL("add_url_iwa.js"), kBackgroundJs);
+
+ ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
+}
+#endif
+
} // namespace extensions
diff --git a/chrome/browser/history/history_utils_unittest.cc b/chrome/browser/history/history_utils_unittest.cc
new file mode 100644
index 0000000..896c62b
--- /dev/null
+++ b/chrome/browser/history/history_utils_unittest.cc
@@ -0,0 +1,33 @@
+// 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 "chrome/browser/history/history_utils.h"
+
+#include "build/build_config.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+#if !BUILDFLAG(IS_ANDROID)
+#include "components/webapps/isolated_web_apps/scheme.h"
+#endif
+
+TEST(HistoryUtilsTest, CanAddURLToHistory) {
+ EXPECT_TRUE(CanAddURLToHistory(GURL("https://www.google.com")));
+ EXPECT_TRUE(CanAddURLToHistory(GURL("http://example.com/path?query=1")));
+
+ EXPECT_FALSE(CanAddURLToHistory(GURL()));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("invalid-url")));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("javascript:alert(1)")));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("about:blank")));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("chrome://version")));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("chrome-untrusted://terminal")));
+ EXPECT_FALSE(CanAddURLToHistory(GURL("view-source:https://www.google.com")));
+
+#if !BUILDFLAG(IS_ANDROID)
+ EXPECT_FALSE(CanAddURLToHistory(
+ GURL("isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/index.html")));
+ EXPECT_FALSE(CanAddURLToHistory(
+ GURL("isolated-app://aerugq4wfqsayeaaxxxxxxxxxx/secret/admin")));
+#endif
+}
diff --git a/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc
index 8a2b250..635ee82e 100644
--- a/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc
@@ -216,6 +216,28 @@
browser()->tab_strip_model()->GetWebContentsAt(1)->GetURL());
}
+IN_PROC_BROWSER_TEST_F(BrowserNavigatorIwaTest,
+ AutoBookmarkNavigationCancelled) {
+ ASSERT_NO_FATAL_FAILURE(InstallBundles());
+
+ const size_t browsers_before =
+ GlobalBrowserCollection::GetInstance()->GetSize();
+
+ // Navigating to an IWA via PAGE_TRANSITION_AUTO_BOOKMARK from a non-app
+ // context (e.g. History menu, History clusters, bookmarks) must be cancelled
+ // as a cross-IWA navigation.
+ NavigateParams params = MakeNavigateParams(browser());
+ params.url = url_info1_->origin().GetURL().Resolve("/deep-link.html");
+ params.transition = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
+ params.disposition = WindowOpenDisposition::NEW_WINDOW;
+ base::WeakPtr<content::NavigationHandle> handle = Navigate(¶ms);
+ EXPECT_EQ(nullptr, handle);
+
+ EXPECT_EQ(browsers_before, GlobalBrowserCollection::GetInstance()->GetSize());
+ EXPECT_FALSE(web_app::AppBrowserController::FindForWebApp(
+ *profile(), url_info1_->app_id()));
+}
+
#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(BrowserNavigatorIwaTest, WindowOpenProtocol) {
ASSERT_NO_FATAL_FAILURE(InstallBundles());
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