Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Browser
DescriptionMissing authorization in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker511824746
Fix commit2c850898c887 (chromium/src) +396/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
chrome/browser/background/BUILD.gn
modified
if
chrome/browser/background/background_contents_navigation_throttle.cc
modified
WillStartRequest
chrome/browser/background/background_contents_navigation_throttle.cc
modified
WillRedirectRequest
chrome/browser/background/background_contents_navigation_throttle.cc
modified
WillStartOrRedirectRequest
chrome/browser/background/background_contents_navigation_throttle.cc
modified

Files Changed

  • chrome/browser/BUILD.gn
  • chrome/browser/background/BUILD.gn
  • chrome/browser/background/background_contents.cc
  • chrome/browser/background/background_contents_navigation_throttle.cc
From 2c850898c887b93e039cd8557ced79fd3f1f3c39 Mon Sep 17 00:00:00 2001
From: Jan Keitel <jkeitel@google.com>
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 <rdevlin.cronin@chromium.org>
Commit-Queue: Jan Keitel <jkeitel@google.com>
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.