CVE-2026-113142
Overview
Files Changed
extensions/browser/url_loader_factory_manager.ccchrome/browser/extensions/orb_and_cors_extension_browsertest.cc
Patch
From e8dd7bb69414e987b6d9128eb30a3f0f85c414ba Mon Sep 17 00:00:00 2001 From: Takashi Toyoshima <[email protected]> Date: Thu, 17 Sep 2026 22:41:47 -0700 Subject: [PATCH] Extensions: Restrict factory params for isolated sandboxed frames Sandboxed extension frames commit in a separate, unprivileged renderer process under kIsolateSandboxedIframes. Previously, URLLoaderFactoryManager::OverrideURLLoaderFactoryParams checked only the precursor tuple, incorrectly granting extension-process privileges (such as `unsafe_non_webby_initiator = true`) to these unprivileged processes. This CL updates OverrideURLLoaderFactoryParams to verify that the recipient process is registered as a privileged extension process via ProcessMap before overriding factory params for opaque-origin frames. When isolated sandboxed iframes are disabled and the frame shares the extension process, intra-process isolation against a compromised renderer is already an accepted non-goal, so relying on ProcessMap correctly protects the Site Isolation boundary. Bug: 498733953 Change-Id: I75ab1f3f8a6ec0c505d56d785091be54bcea0152 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8401885 Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Takashi Toyoshima <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700499} --- diff --git a/extensions/browser/url_loader_factory_manager.cc b/extensions/browser/url_loader_factory_manager.cc index 34c398f..f0195dd 100644 --- a/extensions/browser/url_loader_factory_manager.cc +++ b/extensions/browser/url_loader_factory_manager.cc @@ -11,6 +11,7 @@ #include "base/feature_list.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" +#include "content/public/common/child_process_id_util.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/process_map.h" #include "extensions/browser/script_injection_tracker.h" @@ -233,6 +234,21 @@ return; } + // A sandboxed frame (or page) derived from an extension may be hosted in a + // separate, unprivileged renderer process under kIsolateSandboxedIframes. + // Only grant extension privileges if the recipient process is actually a + // privileged extension process. + if (origin.opaque() && !is_for_isolated_world) { + ProcessMap* process_map = ProcessMap::Get(browser_context); + if (!process_map || factory_params->process_id.is_browser() || + !process_map->IsPrivilegedExtensionProcess( + *extension, + content::ToChildProcessId( + factory_params->process_id.renderer_process_id()))) { + return; + } + } + // Identify and set |factory_params| that need to be overridden. FactoryUser factory_user = is_for_isolated_world ? FactoryUser::kContentScript diff --git a/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc b/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc index 16ab79f..e037bf3 100644 --- a/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc +++ b/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc @@ -47,7 +47,9 @@ #include "content/public/browser/service_worker_context.h" #include "content/public/browser/service_worker_context_observer.h" #include "content/public/browser/shared_cors_origin_access_list.h" +#include "content/public/browser/site_isolation_policy.h" #include "content/public/browser/web_contents.h" +#include "content/public/common/child_process_id_util.h" #include "content/public/common/content_client.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" @@ -3110,4 +3112,73 @@ VerifyFetchWasBlockedByOrb(other_url); } +// Tests that process-isolated sandboxed frames do not receive extension factory +// privileges (defense-in-depth against a compromised renderer). +IN_PROC_BROWSER_TEST_F(OrbAndCorsExtensionBrowserTest, + FromProcessIsolatedSandboxedPage_FactoryParams) { + static constexpr char kManifest[] = + R"({ + "name": "Sandboxed Page Factory Params Test", + "version": "1.0", + "manifest_version": 3, + "sandbox": { + "pages": [ "sandboxed.html" ] + } + })"; + dir_.WriteManifest(kManifest); + dir_.WriteFile(FILE_PATH_LITERAL("sandboxed.html"), "<html>Sandboxed</html>"); + dir_.WriteFile(FILE_PATH_LITERAL("page.html"), + "<html><iframe src=\"sandboxed.html\"></iframe></html>"); + const Extension* extension = LoadExtension(dir_.UnpackedPath()); + ASSERT_TRUE(extension); + + ASSERT_TRUE(NavigateToURL(active_web_contents(), + extension->GetResourceURL("page.html"))); + content::RenderFrameHost* main_frame = + active_web_contents()->GetPrimaryMainFrame(); + content::RenderFrameHost* sandboxed_frame = + content::ChildFrameAt(main_frame, 0); + ASSERT_TRUE(sandboxed_frame); + + // Test factory parameters for the main extension frame. + network::mojom::URLLoaderFactoryParamsPtr main_frame_params = + network::mojom::URLLoaderFactoryParams::New(); + main_frame_params->process_id = + content::ToOriginatingProcessId(main_frame->GetProcess()->GetID()); + main_frame_params->is_trusted = false; + main_frame_params->unsafe_non_webby_initiator = false; + + extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams( + active_web_contents()->GetBrowserContext(), + main_frame->GetLastCommittedOrigin(), + /*is_for_isolated_world=*/false, + /*is_for_service_worker=*/false, main_frame_params.get()); + + EXPECT_TRUE(main_frame_params->unsafe_non_webby_initiator); + + // Test factory parameters for the manifest-sandboxed subframe. + network::mojom::URLLoaderFactoryParamsPtr sandboxed_params = + network::mojom::URLLoaderFactoryParams::New(); + sandboxed_params->process_id = + content::ToOriginatingProcessId(sandboxed_frame->GetProcess()->GetID()); + sandboxed_params->is_trusted = false; + sandboxed_params->unsafe_non_webby_initiator = false; + + extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams( + active_web_contents()->GetBrowserContext(), + sandboxed_frame->GetLastCommittedOrigin(), + /*is_for_isolated_world=*/false, + /*is_for_service_worker=*/false, sandboxed_params.get()); + + // Under Site Isolation, verify the frame lacks extension factory privileges. + // Without isolation, the frame shares the process and inherits privileges. + if (content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled()) { + EXPECT_NE(main_frame->GetProcess(), sandboxed_frame->GetProcess()); + EXPECT_FALSE(sandboxed_params->unsafe_non_webby_initiator); + } else { + EXPECT_EQ(main_frame->GetProcess(), sandboxed_frame->GetProcess()); + EXPECT_TRUE(sandboxed_params->unsafe_non_webby_initiator); + } +} + } // namespace extensions
Regression Test / PoC
diff --git a/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc b/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc
index 16ab79f..e037bf3 100644
--- a/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc
+++ b/chrome/browser/extensions/orb_and_cors_extension_browsertest.cc
@@ -47,7 +47,9 @@
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/shared_cors_origin_access_list.h"
+#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
+#include "content/public/common/child_process_id_util.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
@@ -3110,4 +3112,73 @@
VerifyFetchWasBlockedByOrb(other_url);
}
+// Tests that process-isolated sandboxed frames do not receive extension factory
+// privileges (defense-in-depth against a compromised renderer).
+IN_PROC_BROWSER_TEST_F(OrbAndCorsExtensionBrowserTest,
+ FromProcessIsolatedSandboxedPage_FactoryParams) {
+ static constexpr char kManifest[] =
+ R"({
+ "name": "Sandboxed Page Factory Params Test",
+ "version": "1.0",
+ "manifest_version": 3,
+ "sandbox": {
+ "pages": [ "sandboxed.html" ]
+ }
+ })";
+ dir_.WriteManifest(kManifest);
+ dir_.WriteFile(FILE_PATH_LITERAL("sandboxed.html"), "<html>Sandboxed</html>");
+ dir_.WriteFile(FILE_PATH_LITERAL("page.html"),
+ "<html><iframe src=\"sandboxed.html\"></iframe></html>");
+ const Extension* extension = LoadExtension(dir_.UnpackedPath());
+ ASSERT_TRUE(extension);
+
+ ASSERT_TRUE(NavigateToURL(active_web_contents(),
+ extension->GetResourceURL("page.html")));
+ content::RenderFrameHost* main_frame =
+ active_web_contents()->GetPrimaryMainFrame();
+ content::RenderFrameHost* sandboxed_frame =
+ content::ChildFrameAt(main_frame, 0);
+ ASSERT_TRUE(sandboxed_frame);
+
+ // Test factory parameters for the main extension frame.
+ network::mojom::URLLoaderFactoryParamsPtr main_frame_params =
+ network::mojom::URLLoaderFactoryParams::New();
+ main_frame_params->process_id =
+ content::ToOriginatingProcessId(main_frame->GetProcess()->GetID());
+ main_frame_params->is_trusted = false;
+ main_frame_params->unsafe_non_webby_initiator = false;
+
+ extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams(
+ active_web_contents()->GetBrowserContext(),
+ main_frame->GetLastCommittedOrigin(),
+ /*is_for_isolated_world=*/false,
+ /*is_for_service_worker=*/false, main_frame_params.get());
+
+ EXPECT_TRUE(main_frame_params->unsafe_non_webby_initiator);
+
+ // Test factory parameters for the manifest-sandboxed subframe.
+ network::mojom::URLLoaderFactoryParamsPtr sandboxed_params =
+ network::mojom::URLLoaderFactoryParams::New();
+ sandboxed_params->process_id =
+ content::ToOriginatingProcessId(sandboxed_frame->GetProcess()->GetID());
+ sandboxed_params->is_trusted = false;
+ sandboxed_params->unsafe_non_webby_initiator = false;
+
+ extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams(
+ active_web_contents()->GetBrowserContext(),
+ sandboxed_frame->GetLastCommittedOrigin(),
+ /*is_for_isolated_world=*/false,
+ /*is_for_service_worker=*/false, sandboxed_params.get());
+
+ // Under Site Isolation, verify the frame lacks extension factory privileges.
+ // Without isolation, the frame shares the process and inherits privileges.
+ if (content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled()) {
+ EXPECT_NE(main_frame->GetProcess(), sandboxed_frame->GetProcess());
+ EXPECT_FALSE(sandboxed_params->unsafe_non_webby_initiator);
+ } else {
+ EXPECT_EQ(main_frame->GetProcess(), sandboxed_frame->GetProcess());
+ EXPECT_TRUE(sandboxed_params->unsafe_non_webby_initiator);
+ }
+}
+
} // namespace extensions
Original Bug Report
ORB bypass in sandboxed extension pages via precursor origin misattribution
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: Sandboxed extension pages isolated in non-privileged renderer processes receive URLLoaderFactories with Opaque Resource Blocking (ORB) disabled. The browser incorrectly elevates privileges based on the origin’s precursor tuple rather than the actual process privilege level. A compromised sandboxed renderer can exploit this to leak sensitive cross-origin data, bypassing Site Isolation.
Affected files:
extensions/browser/url_loader_factory_manager.ccservices/network/url_loader.ccchrome/browser/extensions/chrome_content_browser_client_extensions_part.cccontent/browser/loader/url_loader_factory_params_helper.cccontent/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2025-04-21
Vulnerability Details
When kIsolateSandboxedIframes is enabled, manifest-sandboxed extension pages are correctly isolated into unprivileged renderer processes. Standard process isolation and URLLoaderFactory provisioning are applied to these sandboxed extension pages, assigning them an opaque origin with a chrome-extension:// precursor tuple.
However, a logic flaw occurs during factory parameter override. In extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams, the code attempts to identify extension origins using origin.GetTupleOrPrecursorTupleIfOpaque(). Because the sandboxed frame’s opaque origin retains the extension precursor, the system falsely elevates the factory’s user context to FactoryUser::kExtensionProcess.
If the parent extension possesses host permissions (e.g., <all_urls>), ShouldRelaxCors() evaluates to true. The system then makes a direct leap to disabling security boundaries by explicitly setting params->is_orb_enabled = false.
Consequently, the Network Service provisions a URLLoaderFactory for this unprivileged renderer with ORB entirely disabled. If an attacker compromises the sandboxed renderer process (which often has a larger attack surface due to permissive CSPs allowing unsafe-eval), they can issue cross-origin no-cors requests via the Mojo interface. The Network Service skips ORB sniffing and streams the raw, sensitive cross-origin response bodies directly into the compromised renderer’s memory.
Potential Reproduction Steps
(Note: These are suggested steps; our tooling agent does not currently run code to verify an end-to-end exploit.)
- Attacker creates an extension with
<all_urls>host permissions and a sandboxed page defined in the manifest. - The victim installs the extension and loads the sandboxed page.
- The browser isolates the sandboxed page into a non-privileged renderer, but provisions its
URLLoaderFactorywithis_orb_enabled = falsedue to the precursor check. - The attacker compromises the sandboxed renderer (e.g., via a V8 vulnerability).
- The compromised renderer issues a
fetch('https://victim.example/sensitive.json', {mode: 'no-cors', credentials: 'include'}). - The Network Service, honoring the factory parameters, skips ORB and streams the cross-origin JSON directly to the attacker-controlled process.
Suggested Fix
Update extensions::URLLoaderFactoryManager::OverrideURLLoaderFactoryParams to verify the actual privilege level of the requesting process. Instead of relying exclusively on the precursor tuple to grant FactoryUser::kExtensionProcess privileges, the code should query the ProcessMap (e.g., via ProcessMap::IsPrivilegedExtensionProcess()) to ensure ORB is only relaxed for genuinely privileged extension processes, or check if the frame is explicitly sandboxed.
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.