Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient data validation in Extensions
DescriptionInsufficient data validation in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker511249430
Fix commit9fb89ac98d9d (chromium/src) +98/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • chrome/browser/extensions/script_injection_tracker_browsertest.cc
  • extensions/browser/script_injection_tracker.cc
From 9fb89ac98d9d9a20c2d25f4b083f2886cca52bd7 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Wed, 13 May 2026 15:37:51 -0700
Subject: [PATCH] [Extensions] Don't treat error pages as commits for script injections

ScriptInjectionTracker tracks the pages that are committed in order to
determine if an extension script may have run in them. There's a bug
where it doesn't properly filter out error pages, which appear to
commit to the given origin, but are distinct and don't allow script
injection. This would result in potentially recording a script as
having injected into a page, even though it didn't.

Fix this by filtering out error pages in ReadyToCommitNavigation() and
DidFinishNavigation() and add a regression test.

Bug: 511249430
Change-Id: Ia1fadad97950c1ba54d5aeee4420e14047650a18
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7838724
Reviewed-by: Łukasz Anforowicz <lukasza@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1630279}
---

diff --git a/chrome/browser/extensions/script_injection_tracker_browsertest.cc b/chrome/browser/extensions/script_injection_tracker_browsertest.cc
index 4598c81..8b73ba4 100644
--- a/chrome/browser/extensions/script_injection_tracker_browsertest.cc
+++ b/chrome/browser/extensions/script_injection_tracker_browsertest.cc
@@ -2283,4 +2283,92 @@
 }
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
+// Tests that error pages, such as those shown when a frame is blocked via CSP,
+// do not get counted as having scripts injected into them.
+// Regression test for https://crbug.com/511249430.
+IN_PROC_BROWSER_TEST_F(ScriptInjectionTrackerBrowserTest,
+                       CSPBlockedFrameDoesNotGrantPrivilege) {
+  // Set up ControllableHttpResponse to control the attacker page response.
+  std::string attacker_path = "/attacker.html";
+  net::test_server::ControllableHttpResponse attacker_response(
+      embedded_test_server(), attacker_path);
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  // Install a test extension that has content scripts matching victim.com.
+  TestExtensionDir dir;
+  const char kManifestTemplate[] = R"(
+      {
+        "name": "ScriptInjectionTrackerBrowserTest - CSP Blocked",
+        "version": "1.0",
+        "manifest_version": 3,
+        "content_scripts": [{
+          "all_frames": true,
+          "run_at": "document_start",
+          "matches": ["*://victim.com/*"],
+          "js": ["content_script.js"]
+        }]
+      } )";
+  dir.WriteManifest(kManifestTemplate);
+  dir.WriteFile(FILE_PATH_LITERAL("content_script.js"),
+                "self.didInject = 'injected';");
+  const Extension* extension = LoadExtension(dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Navigate to attacker.com/attacker.html.
+  GURL attacker_url =
+      embedded_test_server()->GetURL("attacker.com", attacker_path);
+  content::TestNavigationObserver nav_observer(attacker_url);
+  nav_observer.StartWatchingNewWebContents();
+  // Don't wait for the load to finish; we need to respond with the custom
+  // response.
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser(), attacker_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+  attacker_response.WaitForRequest();
+
+  // Respond with a CSP that will block an iframe loading victim.com.
+  static constexpr char kHtmlTemplate[] =
+      R"(<html>
+           <body>
+             <iframe src="%s"></iframe>
+           </body>
+         </html>)";
+  GURL victim_url("http://victim.com/title1.html");
+  std::string response_body =
+      base::StringPrintf(kHtmlTemplate, victim_url.spec().c_str());
+  std::vector<std::string> headers = {
+      "Content-Security-Policy: frame-src 'none'"};
+  attacker_response.Send(net::HTTP_OK, "text/html", response_body, {}, headers);
+  attacker_response.Done();
+  nav_observer.WaitForNavigationFinished();
+
+  content::WebContents* tab = GetActiveWebContents();
+  content::WaitForLoadStop(tab);
+  content::RenderFrameHost* main_frame = tab->GetPrimaryMainFrame();
+
+  // Find the child frame.
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The child frame should have failed to navigate to victim.com and committed
+  // an error page instead.
+  EXPECT_TRUE(child_frame->IsErrorDocument());
+  EXPECT_EQ(victim_url, child_frame->GetLastCommittedURL());
+
+  // The child frame is hosted in the same process as the main frame, since it's
+  // an error page.
+  EXPECT_EQ(main_frame->GetProcess(), child_frame->GetProcess());
+
+  // The tracker should NOT think that the attacker process (which hosts the
+  // main frame and the error page) has run content scripts from the extension,
+  // even though the iframe was targeted to victim.com.
+  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+      *main_frame->GetProcess(), extension->id()));
+
+  // Double-check that the script didn't run in the frame.
+  EXPECT_EQ(
+      "did not inject",
+      content::EvalJs(child_frame, "self.didInject || 'did not inject';"));
+}
+
 }  // namespace extensions
diff --git a/extensions/browser/script_injection_tracker.cc b/extensions/browser/script_injection_tracker.cc
index 2f7f427..282701b 100644
--- a/extensions/browser/script_injection_tracker.cc
+++ b/extensions/browser/script_injection_tracker.cc
@@ -34,6 +34,7 @@
 #include "extensions/common/permissions/permissions_data.h"
 #include "extensions/common/trace_util.h"
 #include "extensions/common/user_script.h"
+#include "net/base/net_errors.h"
 #include "services/metrics/public/cpp/metrics_utils.h"
 #include "services/metrics/public/cpp/ukm_builders.h"
 
@@ -691,6 +692,11 @@
     content::NavigationHandle* navigation) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
+  // Ignore error pages. They won't allow script injection.
+  if (navigation->GetNetErrorCode() != net::OK) {
+    return;
+  }
+
   content::RenderFrameHost& frame = *navigation->GetRenderFrameHost();
   content::RenderProcessHost& process = *frame.GetProcess();
   TRACE_EVENT("extensions", "ScriptInjectionTracker::ReadyToCommitNavigation",
@@ -744,14 +750,14 @@
 
   // Only consider cross-document navigations that actually commit.  (Documents
   // associated with same-document navigations should have already been
-  // processed by an earlier DidFinishNavigation.  Navigations that don't
-  // commit/load won't inject content scripts.  Content script injections are
+  // processed by an earlier DidFinishNavigation. Navigations that don't
+  // commit/load won't inject content scripts. Content script injections are
   // primarily driven by URL matching and therefore failed navigations may still
   // end up injecting content scripts into the error page. Pre-rendered pages
   // already ran content scripts at the initial navigation and don't need to
-  // run them again on activation.)
+  // run them again on activation. Error pages don't allow script injection.)
   if (!navigation->HasCommitted() || navigation->IsSameDocument() ||
-      navigation->IsPrerenderedPageActivation()) {
+      navigation->IsPrerenderedPageActivation() || navigation->IsErrorPage()) {
     return;
   }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/script_injection_tracker_browsertest.cc b/chrome/browser/extensions/script_injection_tracker_browsertest.cc
index 4598c81..8b73ba4 100644
--- a/chrome/browser/extensions/script_injection_tracker_browsertest.cc
+++ b/chrome/browser/extensions/script_injection_tracker_browsertest.cc
@@ -2283,4 +2283,92 @@
 }
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
+// Tests that error pages, such as those shown when a frame is blocked via CSP,
+// do not get counted as having scripts injected into them.
+// Regression test for https://crbug.com/511249430.
+IN_PROC_BROWSER_TEST_F(ScriptInjectionTrackerBrowserTest,
+                       CSPBlockedFrameDoesNotGrantPrivilege) {
+  // Set up ControllableHttpResponse to control the attacker page response.
+  std::string attacker_path = "/attacker.html";
+  net::test_server::ControllableHttpResponse attacker_response(
+      embedded_test_server(), attacker_path);
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  // Install a test extension that has content scripts matching victim.com.
+  TestExtensionDir dir;
+  const char kManifestTemplate[] = R"(
+      {
+        "name": "ScriptInjectionTrackerBrowserTest - CSP Blocked",
+        "version": "1.0",
+        "manifest_version": 3,
+        "content_scripts": [{
+          "all_frames": true,
+          "run_at": "document_start",
+          "matches": ["*://victim.com/*"],
+          "js": ["content_script.js"]
+        }]
+      } )";
+  dir.WriteManifest(kManifestTemplate);
+  dir.WriteFile(FILE_PATH_LITERAL("content_script.js"),
+                "self.didInject = 'injected';");
+  const Extension* extension = LoadExtension(dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Navigate to attacker.com/attacker.html.
+  GURL attacker_url =
+      embedded_test_server()->GetURL("attacker.com", attacker_path);
+  content::TestNavigationObserver nav_observer(attacker_url);
+  nav_observer.StartWatchingNewWebContents();
+  // Don't wait for the load to finish; we need to respond with the custom
+  // response.
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser(), attacker_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
+  attacker_response.WaitForRequest();
+
+  // Respond with a CSP that will block an iframe loading victim.com.
+  static constexpr char kHtmlTemplate[] =
+      R"(<html>
+           <body>
+             <iframe src="%s"></iframe>
+           </body>
+         </html>)";
+  GURL victim_url("http://victim.com/title1.html");
+  std::string response_body =
+      base::StringPrintf(kHtmlTemplate, victim_url.spec().c_str());
+  std::vector<std::string> headers = {
+      "Content-Security-Policy: frame-src 'none'"};
+  attacker_response.Send(net::HTTP_OK, "text/html", response_body, {}, headers);
+  attacker_response.Done();
+  nav_observer.WaitForNavigationFinished();
+
+  content::WebContents* tab = GetActiveWebContents();
+  content::WaitForLoadStop(tab);
+  content::RenderFrameHost* main_frame = tab->GetPrimaryMainFrame();
+
+  // Find the child frame.
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The child frame should have failed to navigate to victim.com and committed
+  // an error page instead.
+  EXPECT_TRUE(child_frame->IsErrorDocument());
+  EXPECT_EQ(victim_url, child_frame->GetLastCommittedURL());
+
+  // The child frame is hosted in the same process as the main frame, since it's
+  // an error page.
+  EXPECT_EQ(main_frame->GetProcess(), child_frame->GetProcess());
+
+  // The tracker should NOT think that the attacker process (which hosts the
+  // main frame and the error page) has run content scripts from the extension,
+  // even though the iframe was targeted to victim.com.
+  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+      *main_frame->GetProcess(), extension->id()));
+
+  // Double-check that the script didn't run in the frame.
+  EXPECT_EQ(
+      "did not inject",
+      content::EvalJs(child_frame, "self.didInject || 'did not inject';"));
+}
+
 }  // namespace extensions
Loading diff…

Original Bug Report

reported by vm...@google.com

Privilege Escalation via ScriptInjectionTracker and CSP error pages

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic flaw in ScriptInjectionTracker potentially allows a compromised renderer process to bypass Site Isolation and gain unauthorized extension content script privileges. By using a Content Security Policy to block a cross-origin iframe navigation, an attacker can force an error page to commit in their process while retaining the victim’s URL. The tracker fails to ignore error pages, mistakenly granting the attacker’s process the permissions associated with the victim’s URL.

Affected files:

  • extensions/browser/script_injection_tracker.cc
  • extensions/browser/extension_web_contents_observer.cc
  • content/browser/renderer_host/navigation_request.cc

Estimated timestamp from git blame: 2023-10-25

Summary

ScriptInjectionTracker tracks which renderer processes are authorized to run content scripts for specific extensions. This browser-side state is critical for validating IPC requests (like messaging) to ensure they originate from legitimate processes.

A potential vulnerability exists where ScriptInjectionTracker fails to account for error pages during navigation commit. If a subframe navigation is blocked by the parent frame’s Content Security Policy (e.g., frame-src 'none'), an error page is committed. Because subframe error page isolation is generally disabled, this error page commits in the parent’s (attacker’s) process. The tracker observes the intended destination URL of the error page, assumes a successful cross-origin navigation, and incorrectly grants the attacker’s process the privileges of any extension content scripts configured to run on the target URL.

Potential Vulnerability Flow

Based on code analysis, the following sequence outlines how an attacker could potentially trigger this issue:

  1. Attacker Setup: An attacker compromises a renderer process for https://attacker.com/. The attacker’s page serves a CSP header Content-Security-Policy: frame-src 'none'.
  2. Blocked Navigation: The compromised page injects an iframe targeting a victim site: <iframe src="https://victim.com/">.
  3. Error Page Assignment: The browser’s NavigationRequest::CheckContentSecurityPolicy blocks the request, resulting in net::ERR_BLOCKED_BY_CSP. Because SiteIsolationPolicy::IsErrorPageIsolationEnabled returns false for subframes, NavigationRequest::ComputeErrorPageProcess designates the current (attacker’s) process to host the error page.
  4. Flawed Tracking: During the commit phase, ScriptInjectionTracker::ReadyToCommitNavigation (and subsequently DidFinishNavigation) is invoked in extensions/browser/script_injection_tracker.cc.
  5. Missing Error Check: Crucially, these methods extract the intended URL (const GURL& url = navigation->GetURL();) but fail to verify if the navigation is an error page (navigation->IsErrorPage()).
  6. Capability Granted: The tracker passes the victim URL to GetExtensionsInjectingContentScripts and then StoreExtensionsInjectingScripts. This permanently adds the target extension’s ID to the attacker’s RenderProcessHostUserData, authorizing the process to act as the extension’s content script.
  7. IPC Forgery: The compromised renderer crafts a forged IPC (e.g., ExtensionHostMsg_OpenChannelToExtension) claiming to be the extension’s content script running on https://victim.com/.
  8. Validation Bypass:
    • The browser’s IsValidMessagingSource checks process capabilities via ScriptInjectionTracker::DidProcessRunContentScriptFromExtension. Because of Step 6, this passes.
    • The browser’s IsValidSourceUrl verifies the origin by checking if the iframe’s GetLastCommittedURL() matches the IPC payload. Because Chrome preserves the intended URL for error pages, the iframe’s committed URL is https://victim.com/, matching the payload perfectly and bypassing base-origin checks.

As a result, the attacker process successfully escalates privileges to interact with the target extension’s background context.

Note: These steps are suggested based on code analysis; a working Proof of Concept has not yet been executed in a live environment.

Suggested Fix

To remediate this issue, ScriptInjectionTracker should explicitly ignore navigations that result in an error page.

In extensions/browser/script_injection_tracker.cc:

  1. Modify ReadyToCommitNavigation to return early for error pages:
void ScriptInjectionTracker::ReadyToCommitNavigation(
    base::PassKey<ExtensionWebContentsObserver> pass_key,
    content::NavigationHandle* navigation) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  
  if (navigation->IsErrorPage()) {
      return;
  }
  // ... existing code ...
  1. Add the same check to DidFinishNavigation:
void ScriptInjectionTracker::DidFinishNavigation(
    base::PassKey<ExtensionWebContentsObserver> pass_key,
    content::NavigationHandle* navigation) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!navigation->HasCommitted() || navigation->IsSameDocument() ||
      navigation->IsPrerenderedPageActivation() || 
      navigation->IsErrorPage()) {
    return;
  }
  // ... existing code ...

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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