Low chrome Input Validation 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Extensions
DescriptionImproper input validation in Extensions
ComponentExtensions
Bug ClassInput Validation
Tracker553256068
Fix commit286508da4427 (chromium/src) +140/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
chrome/browser/extensions/api/history/history_apitest.cc
modified
TEST
chrome/browser/history/history_utils_unittest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc
modified
if
chrome/browser/extensions/api/history/BUILD.gn
modified
source_set
chrome/browser/history/BUILD.gn
modified

Files Changed

  • chrome/browser/history/BUILD.gn
  • chrome/browser/web_applications/isolated_web_apps/browser_navigator_iwa_browsertest.cc
  • chrome/browser/ui/web_applications/navigation_capturing_process.cc
  • chrome/browser/history/history_utils_unittest.cc
  • chrome/browser/extensions/api/history/BUILD.gn
  • chrome/browser/extensions/api/history/history_api.cc
  • chrome/browser/history/history_utils.cc
  • chrome/browser/extensions/api/history/history_apitest.cc
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(&params);
+  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(&params);
+  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.