Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Extensions
DescriptionMemory corruption in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassMemory Corruption
Tracker498733953
Fix commite8dd7bb69414 (chromium/src) +87/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • extensions/browser/url_loader_factory_manager.cc
  • chrome/browser/extensions/orb_and_cors_extension_browsertest.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • services/network/url_loader.cc
  • chrome/browser/extensions/chrome_content_browser_client_extensions_part.cc
  • content/browser/loader/url_loader_factory_params_helper.cc
  • content/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.)

  1. Attacker creates an extension with <all_urls> host permissions and a sandboxed page defined in the manifest.
  2. The victim installs the extension and loads the sandboxed page.
  3. The browser isolates the sandboxed page into a non-privileged renderer, but provisions its URLLoaderFactory with is_orb_enabled = false due to the precursor check.
  4. The attacker compromises the sandboxed renderer (e.g., via a V8 vulnerability).
  5. The compromised renderer issues a fetch('https://victim.example/sensitive.json', {mode: 'no-cors', credentials: 'include'}).
  6. 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.

View on issue tracker