Chrome · Browser
CVE-2026-87560
Logic Error in Browser
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/background/BUILD.gn |
modified | |
ifchrome/browser/background/background_contents_navigation_throttle.cc |
modified | |
WillStartRequestchrome/browser/background/background_contents_navigation_throttle.cc |
modified | |
WillRedirectRequestchrome/browser/background/background_contents_navigation_throttle.cc |
modified | |
WillStartOrRedirectRequestchrome/browser/background/background_contents_navigation_throttle.cc |
modified |
Files Changed
chrome/browser/BUILD.gnchrome/browser/background/BUILD.gnchrome/browser/background/background_contents.ccchrome/browser/background/background_contents_navigation_throttle.cc
Patch
From 2c850898c887b93e039cd8557ced79fd3f1f3c39 Mon Sep 17 00:00:00 2001 From: Jan Keitel <[email protected]> Date: Fri, 14 Aug 2026 01:28:50 -0700 Subject: [PATCH] Enforce web extent on hosted app BackgroundContents Previously, a hosted app with the background permission could open a background contents window to an off-extent target URL via window.open(url, 'name', 'background'). The target URL would be saved in kRegisteredBackgroundContents preferences and reloaded without extent validation on browser startup. This CL implements a 4-layer defense-in-depth fix: 1. In Browser::CreateBackgroundContents, reject target URLs that are outside the hosted app's manifest web_extent(). 2. In BackgroundContentsNavigationThrottle, intercept and block main-frame navigations outside web_extent(). 3. In OnBackgroundContentsNavigated, verify extent before registering in preferences and unregister if off-extent. 4. In LoadBackgroundContentsFromDictionary, check extent before restoring saved preference URLs on startup. TAG=agy CONV=dda136c7-b0bc-4856-a8dc-3e0b48d7c407 Bug: 511824746 Change-Id: I88bfc6018376f02a5138dfb96e707c014d46620e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175450 Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Jan Keitel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1679423} --- diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index 1e27b46..54fd32db 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -932,6 +932,7 @@ "//chrome/browser/ai", "//chrome/browser/app_mode", "//chrome/browser/autocomplete:aim_eligibility_service", + "//chrome/browser/background", "//chrome/browser/battery", "//chrome/browser/bluetooth", "//chrome/browser/browsing_data", diff --git a/chrome/browser/background/BUILD.gn b/chrome/browser/background/BUILD.gn index e3c8221..5c73229 100644 --- a/chrome/browser/background/BUILD.gn +++ b/chrome/browser/background/BUILD.gn @@ -14,6 +14,7 @@ if (enable_background_contents) { sources += [ + "background_contents_navigation_throttle.h", "background_contents_service.h", "background_contents_service_factory.h", ] @@ -49,6 +50,7 @@ if (enable_background_contents) { sources += [ + "background_contents_navigation_throttle.cc", "background_contents_service.cc", "background_contents_service_factory.cc", ] diff --git a/chrome/browser/background/background_contents.cc b/chrome/browser/background/background_contents.cc index abe93700..12c5cbc 100644 --- a/chrome/browser/background/background_contents.cc +++ b/chrome/browser/background/background_contents.cc @@ -98,13 +98,15 @@ } void BackgroundContents::PrimaryPageChanged(content::Page& page) { - // Note: because BackgroundContents are only available to extension apps, + // Note: Because `BackgroundContents` are only available to extension apps, // navigation is limited to urls within the app's extent. This is enforced in - // RenderView::decidePolicyForNavigation. If BackgroundContents become - // available as a part of the web platform, it probably makes sense to have - // some way to scope navigation of a background page to its opener's security - // origin. Note: if the first navigation is to a URL outside the app's - // extent a background page will be opened but will remain at about:blank. + // `Browser::CreateBackgroundContents`, + // `BackgroundContentsNavigationThrottle`, and `BackgroundContentsService`. If + // `BackgroundContents` become available as a part of the web platform, it + // probably makes sense to have some way to scope navigation of a background + // page to its opener's security origin. Note: if the first navigation is to a + // URL outside the app's extent a background page will be opened but will + // remain at about:blank. delegate_->OnBackgroundContentsNavigated(this); } diff --git a/chrome/browser/background/background_contents_navigation_throttle.cc b/chrome/browser/background/background_contents_navigation_throttle.cc new file mode 100644 index 0000000..99d287d --- /dev/null +++ b/chrome/browser/background/background_contents_navigation_throttle.cc @@ -0,0 +1,103 @@ +// 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/background/background_contents_navigation_throttle.h" + +#include <memory> + +#include "base/feature_list.h" +#include "chrome/browser/background/background_contents_service.h" +#include "chrome/browser/background/background_contents_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/url_constants.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/extension.h" +#include "extensions/common/extension_features.h" +#include "url/gurl.h" + +// static +void BackgroundContentsNavigationThrottle::MaybeCreateAndAdd( + content::NavigationThrottleRegistry& registry) { + if (!base::FeatureList::IsEnabled( + extensions_features::kBlockBackgroundContentsOffExtentNavigation)) { + return; + } + + content::NavigationHandle& handle = registry.GetNavigationHandle(); + if (!handle.IsInMainFrame()) { + return; + } + + content::WebContents* const web_contents = handle.GetWebContents(); + if (!web_contents) { + return; + } + + BackgroundContentsService* const service = + BackgroundContentsServiceFactory::GetForProfile( + Profile::FromBrowserContext(web_contents->GetBrowserContext())); + if (!service || !service->IsTracked(web_contents)) { + return; + } + + registry.AddThrottle( + std::make_unique<BackgroundContentsNavigationThrottle>(registry)); +} + +BackgroundContentsNavigationThrottle::BackgroundContentsNavigationThrottle( + content::NavigationThrottleRegistry& registry) + : content::NavigationThrottle(registry) {} + +BackgroundContentsNavigationThrottle::~BackgroundContentsNavigationThrottle() = + default; + +content::NavigationThrottle::ThrottleCheckResult +BackgroundContentsNavigationThrottle::WillStartRequest() { + return WillStartOrRedirectRequest(); +} + +content::NavigationThrottle::ThrottleCheckResult +BackgroundContentsNavigationThrottle::WillRedirectRequest() { + return WillStartOrRedirectRequest(); +} + +const char* BackgroundContentsNavigationThrottle::GetNameForLogging() { + return "BackgroundContentsNavigationThrottle"; +} + +content::NavigationThrottle::ThrottleCheckResult +BackgroundContentsNavigationThrottle::WillStartOrRedirectRequest() { + content::WebContents* const web_contents = + navigation_handle()->GetWebContents(); + Profile* const profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + BackgroundContentsService* const service = + BackgroundContentsServiceFactory::GetForProfile(profile); + + const std::string& appid = service->GetParentApplicationId(web_contents); + if (appid.empty()) { + return content::NavigationThrottle::PROCEED; + } + + extensions::ExtensionRegistry* const registry = + extensions::ExtensionRegistry::Get(profile); + const extensions::Extension* const extension = + registry->enabled_extensions().GetByID(appid); + if (!extension) { + return content::NavigationThrottle::BLOCK_REQUEST; + } + + const GURL& url = navigation_handle()->GetURL(); + if (url.is_empty() || url.SchemeIs(url::kAboutScheme)) { + return content::NavigationThrottle::PROCEED; + } + + if (!extension->web_extent().MatchesURL(url)) { + return content::NavigationThrottle::BLOCK_REQUEST; + } + + return content::NavigationThrottle::PROCEED; +} diff --git a/chrome/browser/background/background_contents_navigation_throttle.h b/chrome/browser/background/background_contents_navigation_throttle.h
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/background/background_contents_service_unittest.cc b/chrome/browser/background/background_contents_service_unittest.cc
index ee7d3e5..d80b14a7 100644
--- a/chrome/browser/background/background_contents_service_unittest.cc
+++ b/chrome/browser/background/background_contents_service_unittest.cc
@@ -11,6 +11,7 @@
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
#include "base/test/run_until.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
@@ -27,7 +28,9 @@
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/test/browser_task_environment.h"
+#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
+#include "extensions/common/extension_builder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
@@ -121,6 +124,18 @@
return contents_ptr;
}
+ scoped_refptr<const extensions::Extension> CreateHostedApp(
+ const std::string& name,
+ const GURL& url) {
+ std::string json = base::StringPrintf(
+ R"("app": {"urls": ["%s"], "launch": {"web_url": "%s"}})",
+ url.spec().c_str(), url.spec().c_str());
+ scoped_refptr<const extensions::Extension> extension =
+ extensions::ExtensionBuilder(name).AddJSON(json).Build();
+ extensions::ExtensionRegistry::Get(profile_)->AddEnabled(extension);
+ return extension;
+ }
+
protected:
content::BrowserTaskEnvironment task_environment_{
content::BrowserTaskEnvironment::TimeSource::MOCK_TIME};
@@ -260,9 +275,10 @@
TEST_F(BackgroundContentsServiceTest, RestoreFromPrefs) {
BackgroundContentsService service(profile_);
- // Manually set up the preference.
- const std::string appid = "appid";
const GURL expected_url("http://www.google.com/test");
+ scoped_refptr<const extensions::Extension> extension =
+ CreateHostedApp("test_app", expected_url);
+ const std::string appid = extension->id();
{
ScopedDictPrefUpdate update(profile_->GetPrefs(),
@@ -280,3 +296,49 @@
ASSERT_TRUE(contents);
EXPECT_EQ(expected_url, contents->GetInitialURLForTesting());
}
+
+// Tests that background contents stored in prefs with an off-extent URL are
+// ignored on restore.
+TEST_F(BackgroundContentsServiceTest, RestoreFromPrefsIgnoresUrlOutsideExtent) {
+ BackgroundContentsService service(profile_);
+
+ const GURL in_extent_url("http://www.google.com/test");
+ const GURL out_of_extent_url("http://attacker.example.com/test");
+ scoped_refptr<const extensions::Extension> extension =
+ CreateHostedApp("test_app", in_extent_url);
+ const std::string appid = extension->id();
+
+ {
+ ScopedDictPrefUpdate update(profile_->GetPrefs(),
+ prefs::kRegisteredBackgroundContents);
+ base::DictValue dict;
+ dict.Set("url", out_of_extent_url.spec());
+ dict.Set("name", "test_frame");
+ update->Set(appid, std::move(dict));
+ }
+
+ service.LoadBackgroundContentsForExtension(appid);
+
+ BackgroundContents* contents = service.GetAppBackgroundContents(appid);
+ EXPECT_FALSE(contents);
+}
+
+// Tests that navigating background contents to an off-extent URL unregisters it
+// from prefs.
+TEST_F(BackgroundContentsServiceTest, NavigatedUrlOutsideExtentNotRegistered) {
+ BackgroundContentsService service(profile_);
+
+ const GURL in_extent_url("http://www.google.com/test");
+ const GURL out_of_extent_url("http://attacker.example.com/test");
+ scoped_refptr<const extensions::Extension> extension =
+ CreateHostedApp("test_app", in_extent_url);
+ const std::string appid = extension->id();
+
+ auto owned_contents =
+ std::make_unique<MockBackgroundContents>(&service, appid);
+ EXPECT_EQ(GetPrefs(profile_).size(), 0u);
+ auto* contents = AddToService(std::move(owned_contents));
+
+ contents->Navigate(out_of_extent_url);
+ EXPECT_EQ(GetPrefs(profile_).size(), 0u);
+}
diff --git a/chrome/browser/extensions/app_background_page_apitest.cc b/chrome/browser/extensions/app_background_page_apitest.cc
index 1cfa1ff6..8bb944d 100644
--- a/chrome/browser/extensions/app_background_page_apitest.cc
+++ b/chrome/browser/extensions/app_background_page_apitest.cc
@@ -30,15 +30,18 @@
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/common/chrome_paths.h"
+#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/process_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/switches.h"
#include "extensions/test/extension_test_message_listener.h"
+#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
@@ -601,3 +604,111 @@
content::RunAllPendingInMessageLoop();
ASSERT_TRUE(VerifyBackgroundMode(false));
}
+
+// Tests that opening a background contents window with an off-extent target
+// URL is blocked.
+IN_PROC_BROWSER_TEST_F(AppBackgroundPageApiTest,
+ NoJsBackgroundPageTargetOutsideExtent) {
+ const std::string app_manifest = base::StringPrintf(
+ R"({
+ "name": "App",
+ "version": "0.1",
+ "manifest_version": 2,
+ "app": {
+ "urls": [
+ "http://a.com/"
+ ],
+ "launch": {
+ "web_url": "http://a.com:%u/empty.html"
+ }
+ },
+ "permissions": ["background"],
+ "background": {
+ "allow_js_access": false
+ }
+ })",
+ embedded_test_server()->port());
+
+ base::FilePath app_dir;
+ ASSERT_TRUE(CreateApp(app_manifest, &app_dir));
+ ASSERT_TRUE(LoadExtension(app_dir));
+
+ const Extension* extension = GetSingleLoadedExtension();
+ ASSERT_TRUE(extension);
+
+ GURL launch_url = embedded_test_server()->GetURL("a.com", "/empty.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), launch_url));
+
+ GURL out_of_extent_url =
+ embedded_test_server()->GetURL("b.com", "/empty.html");
+ std::string script =
+ base::StringPrintf("window.open('%s', 'bg', 'background') == null;",
+ out_of_extent_url.spec().c_str());
+ EXPECT_EQ(content::EvalJs(
+ browser()->tab_strip_model()->GetActiveWebContents(), script),
+ true);
+
+ EXPECT_FALSE(BackgroundContentsServiceFactory::GetForProfile(profile())
+ ->GetAppBackgroundContents(extension->id()));
+ UnloadExtension(extension->id());
+}
+
+// Tests that navigating an existing background contents window to an
+// off-extent URL is blocked.
+IN_PROC_BROWSER_TEST_F(AppBackgroundPageApiTest,
+ NoJsBackgroundPageNavigateOutsideExtent) {
+ const std::string app_manifest = base::StringPrintf(
+ R"({
+ "name": "App",
+ "version": "0.1",
+ "manifest_version": 2,
+ "app": {
+ "urls": [
+ "http://a.com/"
+ ],
+ "launch": {
+ "web_url": "http://a.com:%u/empty.html"
+ }
+ },
+ "permissions": ["background"],
+ "background": {
+ "allow_js_access": false
+ }
+ })",
+ embedded_test_server()->port());
+
+ base::FilePath app_dir;
+ ASSERT_TRUE(CreateApp(app_manifest, &app_dir));
+ ASSERT_TRUE(LoadExtension(app_dir));
+
+ const Extension* extension = GetSingleLoadedExtension();
+ ASSERT_TRUE(extension);
+
+ BackgroundContentsTestWaiter background_waiter(profile());
+ GURL launch_url = embedded_test_server()->GetURL("a.com", "/empty.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), launch_url));
+
+ std::string open_script = base::StringPrintf(
+ "window.open('%s', 'bg', 'background');", launch_url.spec().c_str());
+ EXPECT_TRUE(content::ExecJs(
+ browser()->tab_strip_model()->GetActiveWebContents(), open_script));
+ background_waiter.WaitForBackgroundContents(extension->id());
+
+ BackgroundContents* background_contents =
+ BackgroundContentsServiceFactory::GetForProfile(profile())
+ ->GetAppBackgroundContents(extension->id());
+ ASSERT_TRUE(background_contents);
+
+ GURL out_of_extent_url =
+ embedded_test_server()->GetURL("b.com", "/empty.html");
+ std::string nav_script = base::StringPrintf("window.location.href = '%s';",
+ out_of_extent_url.spec().c_str());
+ content::TestNavigationObserver nav_observer(
+ background_contents->web_contents());
+ EXPECT_TRUE(content::ExecJs(background_contents->web_contents(), nav_script));
+ nav_observer.Wait();
+
+ EXPECT_FALSE(nav_observer.last_navigation_succeeded());
+ EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, nav_observer.last_net_error_code());
+ UnloadExtension(extension->id());
+}
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