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 <simonha@google.com>
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 <mahmadi@chromium.org>
Commit-Queue: Simon Hangl <simonha@google.com>
Reviewed-by: Mike Wasserman <msw@chromium.org>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
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