Medium watch ✔︎ verified CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in SiteIsolation
DescriptionInsufficient validation of untrusted input in SiteIsolation
ComponentSiteIsolation
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Mechanism and fix are accurate. Minor problems: the pre-fix dump is called 'unconditional' although it was feature-gated (kDumpOnInvalidNavigationHeaders), and header attachment is attributed to ipc_utils.cc:490-494, which shows only dump and return true.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker487795397
Fix commite0a0f3ed7f30 (chromium/src) +84/-26
Related commitsfdab935fad75 prep (dump-only detection) · chromium/src
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-02-26 (per vendor advisory)
Fix landed2026-05-01 (public git)
Disclosed2026-05-12
Patch gap11 days — fix public in source before the advisory

Background

Site Isolation
the browser-process security model that confines a renderer to its own site and distrusts the data and IPCs that a (possibly compromised) renderer sends.
`VerifyNavigationHeaders`
a browser-process helper (content/browser/renderer_host/ipc_utils.cc) that checks the extra HTTP headers a renderer supplies for a navigation against a strict allowlist.
`bad_message::ReceivedBadMessage`
the content-layer mechanism that terminates a renderer process when it sends an IPC that violates a browser-enforced invariant.
`DumpWithoutCrashing`
a diagnostics call that records a crash report but lets execution continue, used to collect data without enforcing anything.

Root Cause Analysis

Re-analyzed on 2026-10-03 against the actual fix commit e0a0f3ed7f30. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (fdab935fad75).
Renderer-initiated navigations carry a renderer-supplied header string: OpenURL passes params->extra_headers and BeginNavigation passes begin_params->headers. The prep commit fdab935fad75a812d0018c29f1a7161f5f36e9b2 introduced VerifyNavigationHeaders (content/browser/renderer_host/ipc_utils.cc:466) and wired it into VerifyOpenURLParams (added call at diff @@ -261,6 +263) and into RenderFrameHostImpl::BeginNavigation (added call at render_frame_host_impl.cc diff @@ -11441,6). That function parses the headers and compares each against an allowlist of six non-sensitive headers (ipc_utils.cc:478-483: kUpgradeInsecureRequests, kOrigin, kContentType, kUserAgent, kSecPurpose, kDNT). The invariant — a compromised renderer must not set arbitrary request headers such as Cookie or Authorization — was not actually enforced, because on a non-allowlisted header the pre-fix code only set a crash key and, when features::kDumpOnInvalidNavigationHeaders was enabled (ENABLED_BY_DEFAULT, features.cc:206), called base::debug::DumpWithoutCrashing(); either way it fell through to return true (ipc_utils.cc:484-494), so verification did not reject the navigation. The primary commit e0a0f3ed7f30d67acc95874ca5235f6673c1c6a6 restores the invariant: the non-allowlisted branch now (when features::kKillOnInvalidNavigationHeaders is enabled, ENABLED_BY_DEFAULT) calls bad_message::ReceivedBadMessage(process, bad_message::RFH_INVALID_NAVIGATION_HEADERS) and return false, rejecting the request and killing the renderer. The new enum value RFH_INVALID_NAVIGATION_HEADERS = 342 is added in content/browser/bad_message.h.
Key insight
The allowlist existed but was advisory-only — it logged violations yet let the navigation continue (return true); the fix turns the same allowlist into an enforced check that rejects the IPC and terminates the offending renderer.

Attack Path

  1. Compromise a renderer The attacker gains arbitrary code execution in a renderer process (a standard Site Isolation threat-model precondition).
  2. Forge a navigation IPC The renderer sends OpenURL (or BeginNavigation) for a cross-site top-level navigation with extra_headers/headers containing a forbidden header such as Cookie: ... or Authorization: ....
  3. Header passes through pre-fix VerifyNavigationHeaders flags the header but returns true (ipc_utils.cc:484-494), so the navigation is not rejected. The fix’s *_KillDisabled tests, which inject Cookie: secret=123 and expect the navigation to commit, indirectly show the navigation proceeding. That the browser then attaches the renderer-chosen headers to the outgoing request is (inferred); the attachment code is not shown.
  4. Session/credential manipulation The forged Cookie/Authorization reaches the target origin, enabling session fixation or request forgery against that site. (inferred — the material shows header pass-through, not server-side effect.)

Impact Assessment

A compromised renderer could attach arbitrary, security-sensitive HTTP headers (e.g., Cookie, Authorization) to top-level navigation requests to other origins, defeating the Site Isolation guarantee that a renderer cannot forge cross-site request state. The attack requires the renderer to already be compromised; the advisory rates this medium severity and reports no in-the-wild exploitation. Downstream consequences such as session fixation are plausible but server-dependent and not demonstrated by the code shown (inferred).

Changed Functions

FunctionChangeNotes
SecurityExploitBrowserTest
content/browser/security_exploit_browsertest.cc
modified
SecurityExploitBrowserTest
content/browser/security_exploit_browsertest.cc
modified
SecurityExploitHeaderKillDisabledTest
content/browser/security_exploit_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/bad_message.h
  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/security_exploit_browsertest.cc
  • content/common/features.cc
  • content/common/features.h
  • tools/metrics/histograms/metadata/stability/enums.xml

Audit Directions

  • Dump-to-kill TODOs
    Search for validation helpers that log via DumpWithoutCrashing() but still return true; these are detection-only and do not enforce the invariant until converted to ReceivedBadMessage.
  • Renderer-supplied header strings
    Audit every browser-process entry point that accepts a renderer header blob (extra_headers, begin_params->headers) to confirm it routes through an enforced allowlist before the headers are attached to a request.
  • Feature-gated enforcement
    When a security check is behind a BASE_FEATURE, verify the flag’s default state and that the disabled path does not silently reopen the bypass.
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 69ef4deb..3275a09 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -367,6 +367,7 @@
   DT_DUPLICATE_CHILD_TARGET_CREATED = 339,
   RWH_POINTER_LOCK_FROM_SANDBOXED_FRAME = 340,
   SWSI_CROSS_ORIGIN_SCRIPT_URL = 341,
+  RFH_INVALID_NAVIGATION_HEADERS = 342,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/renderer_host/ipc_utils.cc b/content/browser/renderer_host/ipc_utils.cc
index d13f9883..4e9bf14 100644
--- a/content/browser/renderer_host/ipc_utils.cc
+++ b/content/browser/renderer_host/ipc_utils.cc
@@ -481,13 +481,13 @@
         header.name() != net::HttpRequestHeaders::kUserAgent &&
         header.name() != net::HttpRequestHeaders::kSecPurpose &&
         header.name() != net::HttpRequestHeaders::kDNT) {
-      // TODO(https://crbug.com/40093290): Once we have enough data, this should
-      // be a `bad_message::ReceivedBadMessage` and return `false`.
-      SCOPED_CRASH_KEY_STRING64("Bug487795397", "invalid_header",
-                                header.name());
       if (base::FeatureList::IsEnabled(
-              features::kDumpOnInvalidNavigationHeaders)) {
-        base::debug::DumpWithoutCrashing();
+              features::kKillOnInvalidNavigationHeaders)) {
+        SCOPED_CRASH_KEY_STRING64("Bug487795397", "invalid_header",
+                                  header.name());
+        bad_message::ReceivedBadMessage(
+            process, bad_message::RFH_INVALID_NAVIGATION_HEADERS);
+        return false;
       }
     }
   }
diff --git a/content/common/features.cc b/content/common/features.cc
index d7082401..b0f87821 100644
--- a/content/common/features.cc
+++ b/content/common/features.cc
@@ -202,10 +202,6 @@
 BASE_FEATURE(kDocumentPolicyNegotiation, base::FEATURE_DISABLED_BY_DEFAULT);
 
 // When enabled, DumpWithoutCrashing() is called if a renderer process provides
-// invalid (non-allowlisted) headers in a navigation request.
-BASE_FEATURE(kDumpOnInvalidNavigationHeaders, base::FEATURE_ENABLED_BY_DEFAULT);
-
-// When enabled, DumpWithoutCrashing() is called if a renderer process provides
 // an Origin header on a navigation request that doesn't match the expected
 // origin.
 BASE_FEATURE(kDumpOnOriginHeaderMismatch, base::FEATURE_ENABLED_BY_DEFAULT);
@@ -416,6 +412,10 @@
 // When enabled, child process will not terminate itself when IPC is reset.
 BASE_FEATURE(kKeepChildProcessAfterIPCReset, base::FEATURE_DISABLED_BY_DEFAULT);
 
+// When enabled, the renderer process will be killed if it provides
+// invalid (non-allowlisted) headers in a navigation request.
+BASE_FEATURE(kKillOnInvalidNavigationHeaders, base::FEATURE_ENABLED_BY_DEFAULT);
+
 // Enables Local Network Access checks for all types of web workers.
 //
 // The exact checks run are the same as for other document subresources, and
diff --git a/content/common/features.h b/content/common/features.h
index 9781dad0..6a95343 100644
--- a/content/common/features.h
+++ b/content/common/features.h
@@ -80,7 +80,6 @@
 CONTENT_EXPORT BASE_DECLARE_FEATURE(
     kDocumentIsolationPolicyWithoutSiteIsolation);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kDocumentPolicyNegotiation);
-CONTENT_EXPORT BASE_DECLARE_FEATURE(kDumpOnInvalidNavigationHeaders);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kDumpOnOriginHeaderMismatch);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kDumpOnUnexpectedOriginHeader);
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
@@ -155,6 +154,7 @@
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kIOSurfaceCapturer);
 #endif
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kKeepChildProcessAfterIPCReset);
+CONTENT_EXPORT BASE_DECLARE_FEATURE(kKillOnInvalidNavigationHeaders);
 
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kLocalNetworkAccessForWorkers);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kLocalNetworkAccessForWorkersWarningOnly);
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index cc3916c..daef10d 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -508,6 +508,7 @@
   <int value="339" label="DT_DUPLICATE_CHILD_TARGET_CREATED"/>
   <int value="340" label="RWH_POINTER_LOCK_FROM_SANDBOXED_FRAME"/>
   <int value="341" label="SWSI_CROSS_ORIGIN_SCRIPT_URL"/>
+  <int value="342" label="RFH_INVALID_NAVIGATION_HEADERS"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 29527cd7..5b25594 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -289,8 +289,7 @@
 class SecurityExploitBrowserTest : public ContentBrowserTest {
  public:
   SecurityExploitBrowserTest() {
-    feature_list_.InitWithFeatures({features::kDumpOnInvalidNavigationHeaders,
-                                    features::kDumpOnUnexpectedOriginHeader,
+    feature_list_.InitWithFeatures({features::kDumpOnUnexpectedOriginHeader,
                                     features::kDumpOnOriginHeaderMismatch},
                                    {});
   }
@@ -3323,10 +3322,23 @@
   std::optional<std::string> headers_to_inject_;
   bool is_activated_ = false;
 };
+
+class SecurityExploitHeaderKillDisabledTest
+    : public SecurityExploitBrowserTest {
+ public:
+  SecurityExploitHeaderKillDisabledTest() : SecurityExploitBrowserTest() {
+    feature_list_.InitWithFeatures({},
+                                   {features::kKillOnInvalidNavigationHeaders});
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
 }  // namespace
 
-// Tests that DumpWithoutCrashing() is called if a renderer process provides
-// arbitrary headers in a navigation request.
+// Tests that the renderer is killed if it provides arbitrary headers in a
+// navigation request when kKillOnInvalidNavigationHeaders is enabled.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                        ForbiddenHeaderInBeginNavigation) {
   GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
@@ -3336,17 +3348,39 @@
   interceptor.set_headers_to_inject("Cookie: secret=123");
   interceptor.Activate();
 
-  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
-  base::test::TestFuture<void> dump_future;
-  ScopedDumpWithoutCrashingCallback dump_callback(
-      dump_future.GetRepeatingCallback());
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
 
   ExecuteScriptAsync(rfhi, "location = '/title2.html';");
-  EXPECT_TRUE(dump_future.Wait());
+  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
+  EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
-// Tests that DumpWithoutCrashing() is called if a renderer process provides
-// arbitrary headers in an OpenURL request.
+// Tests that the navigation succeeds if a renderer process provides arbitrary
+// headers in a navigation request when kKillOnInvalidNavigationHeaders is
+// disabled.
+IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
+                       ForbiddenHeaderInBeginNavigation_KillDisabled) {
+  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  NavigationHeaderInterceptor interceptor(shell()->web_contents());
+  interceptor.set_headers_to_inject("Cookie: secret=123");
+  interceptor.Activate();
+
+  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
+
+  TestNavigationObserver navigation_observer(shell()->web_contents());
+  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
+  navigation_observer.Wait();
+
+  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
+            shell()->web_contents()->GetLastCommittedURL());
+}
+
+// Tests that the renderer is killed if it provides arbitrary headers in an
+// OpenURL request when kKillOnInvalidNavigationHeaders is enabled.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, ForbiddenHeaderInOpenURL) {
   GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
   EXPECT_TRUE(NavigateToURL(shell(), start_url));
@@ -3358,11 +3392,33 @@
       embedded_test_server()->GetURL("a.test", "/title2.html"));
   params->extra_headers = "Cookie: secret=123";
 
-  base::test::TestFuture<void> dump_future;
-  ScopedDumpWithoutCrashingCallback dump_callback(
-      dump_future.GetRepeatingCallback());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
   static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
-  EXPECT_TRUE(dump_future.Wait());
+  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
+  EXPECT_FALSE(rfhi->IsRenderFrameLive());
+}
+
+// Tests that the navigation succeeds if a renderer process provides arbitrary
+// headers in an OpenURL request when kKillOnInvalidNavigationHeaders is
+// disabled.
+IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
+                       ForbiddenHeaderInOpenURL_KillDisabled) {
+  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  auto params = CreateOpenURLParams(
+      embedded_test_server()->GetURL("a.test", "/title2.html"));
+  params->extra_headers = "Cookie: secret=123";
+
+  TestNavigationObserver navigation_observer(shell()->web_contents());
+  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
+  navigation_observer.Wait();
+
+  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
+            shell()->web_contents()->GetLastCommittedURL());
 }
 
 // Tests that if a renderer provides an Origin header on an OpenURL request,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 29527cd7..5b25594 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -289,8 +289,7 @@
 class SecurityExploitBrowserTest : public ContentBrowserTest {
  public:
   SecurityExploitBrowserTest() {
-    feature_list_.InitWithFeatures({features::kDumpOnInvalidNavigationHeaders,
-                                    features::kDumpOnUnexpectedOriginHeader,
+    feature_list_.InitWithFeatures({features::kDumpOnUnexpectedOriginHeader,
                                     features::kDumpOnOriginHeaderMismatch},
                                    {});
   }
@@ -3323,10 +3322,23 @@
   std::optional<std::string> headers_to_inject_;
   bool is_activated_ = false;
 };
+
+class SecurityExploitHeaderKillDisabledTest
+    : public SecurityExploitBrowserTest {
+ public:
+  SecurityExploitHeaderKillDisabledTest() : SecurityExploitBrowserTest() {
+    feature_list_.InitWithFeatures({},
+                                   {features::kKillOnInvalidNavigationHeaders});
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
 }  // namespace
 
-// Tests that DumpWithoutCrashing() is called if a renderer process provides
-// arbitrary headers in a navigation request.
+// Tests that the renderer is killed if it provides arbitrary headers in a
+// navigation request when kKillOnInvalidNavigationHeaders is enabled.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                        ForbiddenHeaderInBeginNavigation) {
   GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
@@ -3336,17 +3348,39 @@
   interceptor.set_headers_to_inject("Cookie: secret=123");
   interceptor.Activate();
 
-  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
-  base::test::TestFuture<void> dump_future;
-  ScopedDumpWithoutCrashingCallback dump_callback(
-      dump_future.GetRepeatingCallback());
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
 
   ExecuteScriptAsync(rfhi, "location = '/title2.html';");
-  EXPECT_TRUE(dump_future.Wait());
+  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
+  EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
-// Tests that DumpWithoutCrashing() is called if a renderer process provides
-// arbitrary headers in an OpenURL request.
+// Tests that the navigation succeeds if a renderer process provides arbitrary
+// headers in a navigation request when kKillOnInvalidNavigationHeaders is
+// disabled.
+IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
+                       ForbiddenHeaderInBeginNavigation_KillDisabled) {
+  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  NavigationHeaderInterceptor interceptor(shell()->web_contents());
+  interceptor.set_headers_to_inject("Cookie: secret=123");
+  interceptor.Activate();
+
+  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
+
+  TestNavigationObserver navigation_observer(shell()->web_contents());
+  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
+  navigation_observer.Wait();
+
+  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
+            shell()->web_contents()->GetLastCommittedURL());
+}
+
+// Tests that the renderer is killed if it provides arbitrary headers in an
+// OpenURL request when kKillOnInvalidNavigationHeaders is enabled.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, ForbiddenHeaderInOpenURL) {
   GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
   EXPECT_TRUE(NavigateToURL(shell(), start_url));
@@ -3358,11 +3392,33 @@
       embedded_test_server()->GetURL("a.test", "/title2.html"));
   params->extra_headers = "Cookie: secret=123";
 
-  base::test::TestFuture<void> dump_future;
-  ScopedDumpWithoutCrashingCallback dump_callback(
-      dump_future.GetRepeatingCallback());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
   static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
-  EXPECT_TRUE(dump_future.Wait());
+  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
+  EXPECT_FALSE(rfhi->IsRenderFrameLive());
+}
+
+// Tests that the navigation succeeds if a renderer process provides arbitrary
+// headers in an OpenURL request when kKillOnInvalidNavigationHeaders is
+// disabled.
+IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
+                       ForbiddenHeaderInOpenURL_KillDisabled) {
+  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  auto params = CreateOpenURLParams(
+      embedded_test_server()->GetURL("a.test", "/title2.html"));
+  params->extra_headers = "Cookie: secret=123";
+
+  TestNavigationObserver navigation_observer(shell()->web_contents());
+  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
+  navigation_observer.Wait();
+
+  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
+            shell()->web_contents()->GetLastCommittedURL());
 }
 
 // Tests that if a renderer provides an Origin header on an OpenURL request,
Loading diff…

Original Bug Report

reported by [email protected]

Site Isolation bypass: renderer -> control headers sent to other sites, including cookies

VULNERABILITY DETAILS

Compromised renderer can inject arbitrary headers (including cookie) into a request to another site.

VERSION

Chrome Version: Built from git with no special build flags

Operating System: Tested on Linux

REPRODUCTION CASE

  1. Apply patch to the renderer.
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index dd17c9c1d3d17..6ec04dbfdaa6f 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -6249,6 +6249,15 @@ void RenderFrameImpl::BeginNavigationInternal(
           info->is_container_initiated, info->storage_access_api_status,
           info->has_rel_opener);
 
+  if (GURL(info->url_request.Url()).DomainIs("echo.free.beeceptor.com")) {
+    if (!begin_params->headers.empty() && begin_params->headers.back() != '\n') {
+      begin_params->headers += "\r\n";
+    }
+    begin_params->headers += "Cookie: session_id=foobar\r\n";
+    begin_params->headers += "Authorization: Basic Zm9vOmJhcg==\r\n";
+    begin_params->headers += "X-Injected-Header: helloworld\r\n";
+  }
+
   bool current_frame_has_download_sandbox_flag = !frame_->IsAllowedToDownload();
   bool has_download_sandbox_flag =
       info->initiator_frame_has_download_sandbox_flag ||
  1. Open this HTML page (host it on 127.0.0.1)
<!DOCTYPE html>
<html>
<head>
    <title>OpenURL Header Injection PoC</title>
</head>
<body>
    <h1>OpenURL Header Injection PoC</h1>
    <p>This PoC demonstrates that a compromised renderer can inject arbitrary headers (like Cookie) via OpenURL.</p>
    
    <button onclick="window.open('https://echo.free.beeceptor.com/', '_blank')">
        Click to Trigger window.open (OpenURL)
    </button>

    <script>
        console.log("PoC Loaded");
    </script>
</body>
</html>
  1. Click button.

  2. Observe a request is made to the third-party site with arbitrary headers set by the renderer. For example, the attacker can set a cookie header.

From https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/compromised-renderers.md it seems like this would be considered a security issue.

CREDIT INFORMATION

Reporter credit: Ryan Lothian

LLM WRITEUP

Compromised Renderer Can Inject Arbitrary Headers (including Cookie) via OpenURL

The RenderFrameHostImpl::OpenURL method (in content/browser/renderer_host/render_frame_host_impl.cc) processes OpenURL IPC messages from the renderer. It receives blink::mojom::OpenUR LParamsPtr, which contains an extra_headers field (a string of additional HTTP headers).

The VerifyOpenURLParams function (in content/browser/renderer_host/ipc_utils.cc) validates several fields of OpenURLParams (like URL, POST body, initiator origin), but it does not validate or sanitize extra_headers.

RenderFrameHostImpl::OpenURL passes these extra_headers to NavigationControllerImpl::LoadURLWithParams (in content/browser/renderer_host/navigation_controller_impl.cc). NavigationControllerImpl::LoadURLWithParams uses CreateNavigationEntry to store these headers in the NavigationEntry. Later, NavigationRequest (in content/browser/renderer_host/navigation_request.cc) retrieves these headers and adds them to the net::HttpRequestHeaders for the navigation request using AddHeadersFromString.

The network service (in services/network/public/cpp/header_util.cc) checks headers against AreRequestHeadersSafe. However, this check is primarily a DCHECK in NavigationURLLoader. Furthermore, IsRequestHeaderSafe allows several sensitive headers, most notably Cookie, Authorization, and X-Csrf-Token. It explicitly lists Cookie2 and Set-Cookie as unsafe, but leaves Cookie as a TODO item (implying it is allowed).

Impact: A compromised renderer process can send an OpenURL IPC with a crafted extra_headers string containing a Cookie header (e.g., Cookie: session_id=attacker_session). This allows the attacker to perform a Session Fixation attack: the browser will send the attacker’s session cookie to the target server during the top-level navigation. If the server accepts the cookie, the user will be logged in as the attacker. The attacker can also inject Authorization headers to log the user in as the attacker (if they have valid credentials), or other custom headers that might influence the server’s behavior (e.g., X-Forwarded-For spoofing, or bypassing WAFs/CSRF checks).

This violates the principle that a compromised renderer should not be able to forge arbitrary network requests or interfere with the user’s session state on other origins, especially for top-level navigations.

Affected Files:

  • content/browser/renderer_host/render_frame_host_impl.cc: OpenURL method blindly passes extra_headers.
  • content/browser/renderer_host/ipc_utils.cc: VerifyOpenURLParams fails to validate extra_headers.
  • services/network/public/cpp/header_util.cc: IsRequestHeaderSafe allows Cookie header.
View on issue tracker