Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in SiteIsolation
DescriptionIncorrect authorization in SiteIsolation
ComponentSiteIsolation
Bug ClassLogic Error
Tracker518002426
Fix commit784608b0067f (chromium/src) +52/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Files Changed

  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/security_exploit_browsertest.cc
From 784608b0067f6ea6e7d1b66888c725c09fdd4291 Mon Sep 17 00:00:00 2001
From: Zainab Rizvi <[email protected]>
Date: Mon, 03 Aug 2026 06:49:39 -0700
Subject: [PATCH] Normalize LF to CRLF in VerifyNavigationHeaders

OpenURLParams::extra_headers are LF-separated and are normalized to CRLF
by CreateNavigationRequestFromLoadParams() before they are applied to
the outgoing request. HttpRequestHeaders::AddHeadersFromString() splits
only on CRLF, so apply the same normalization in
VerifyNavigationHeaders() so that the allowlist sees the same set of
headers as the request.

TAG=agy
CONV=0f4b35dd-2710-47f6-9c65-c3ab1af7c0f2

Fixed: 518002426
Change-Id: Id0e6a09fef2d42ad8eb71713c39467c1f49a5c3b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8180965
Commit-Queue: Zainab Rizvi <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1672649}
---

diff --git a/content/browser/renderer_host/ipc_utils.cc b/content/browser/renderer_host/ipc_utils.cc
index 664430f6..fb99c3c 100644
--- a/content/browser/renderer_host/ipc_utils.cc
+++ b/content/browser/renderer_host/ipc_utils.cc
@@ -9,6 +9,7 @@
 
 #include "base/debug/crash_logging.h"
 #include "base/debug/dump_without_crashing.h"
+#include "base/strings/string_util.h"
 #include "base/strings/to_string.h"
 #include "content/browser/bad_message.h"
 #include "content/browser/blob_storage/chrome_blob_storage_context.h"
@@ -457,8 +458,15 @@
 
 bool VerifyNavigationHeaders(RenderProcessHost* process,
                              const std::string& headers) {
+  // Navigation headers may be LF-separated and are normalized to CRLF
+  // before being applied to the outgoing request.
+  // AddHeadersFromString() splits only on CRLF, so apply the same normalization
+  // here to ensure consistent header verification.
+  std::string headers_crlf;
+  base::ReplaceChars(headers, "\n", "\r\n", &headers_crlf);
+
   net::HttpRequestHeaders parsed_headers;
-  parsed_headers.AddHeadersFromString(headers);
+  parsed_headers.AddHeadersFromString(headers_crlf);
   for (net::HttpRequestHeaders::Iterator header(parsed_headers);
        header.GetNext();) {
     // Headers should be strictly allowlisted because there can be security
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 9909662..fea640bf 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -4476,6 +4476,26 @@
   EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
+// Tests that the renderer is killed if it provides arbitrary headers in a
+// navigation request when the headers are separated by LF rather than CRLF.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ForbiddenHeaderInBeginNavigation_LFSeparated) {
+  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("DNT: 1\nCookie: secret=123");
+  interceptor.Activate();
+
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
+
+  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
+  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 a navigation request when kKillOnInvalidNavigationHeaders is
 // disabled.
@@ -4517,6 +4537,29 @@
   EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
+// Tests that the renderer is killed if it provides arbitrary headers in an
+// OpenURL request when the headers are separated by LF rather than CRLF.
+// OpenURLParams::extra_headers are normalized from LF to CRLF before they are
+// applied to the outgoing request, so the allowlist check must apply the same
+// normalization.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ForbiddenHeaderInOpenURL_LFSeparated) {
+  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 = "DNT: 1\nCookie: secret=123";
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
+  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
+  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.
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 9909662..fea640bf 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -4476,6 +4476,26 @@
   EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
+// Tests that the renderer is killed if it provides arbitrary headers in a
+// navigation request when the headers are separated by LF rather than CRLF.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ForbiddenHeaderInBeginNavigation_LFSeparated) {
+  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("DNT: 1\nCookie: secret=123");
+  interceptor.Activate();
+
+  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
+
+  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
+  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 a navigation request when kKillOnInvalidNavigationHeaders is
 // disabled.
@@ -4517,6 +4537,29 @@
   EXPECT_FALSE(rfhi->IsRenderFrameLive());
 }
 
+// Tests that the renderer is killed if it provides arbitrary headers in an
+// OpenURL request when the headers are separated by LF rather than CRLF.
+// OpenURLParams::extra_headers are normalized from LF to CRLF before they are
+// applied to the outgoing request, so the allowlist check must apply the same
+// normalization.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ForbiddenHeaderInOpenURL_LFSeparated) {
+  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 = "DNT: 1\nCookie: secret=123";
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
+  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
+  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.
Loading diff…

Original Bug Report

reported by [email protected]

VerifyNavigationHeaders allowlist bypass via bare linefeed injection in OpenURL

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

Overview: A potential validate-before-normalize parser differential between the browser’s navigation header validation and the actual request construction code can allow a compromised renderer to bypass the navigation header allowlist. By utilizing bare linefeed (\n) separators, the initial validation pass fails to extract and check individual headers. Downstream, the navigation code normalizes these linefeeds to CRLF, allowing arbitrary restricted request headers to be sent over the network.

Affected files:

  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/renderer_host/navigation_controller_impl.cc

Estimated timestamp from git blame: 2026-03-18

Description

There is a potential validate-before-normalize parser differential between the browser’s navigation header validation code and the actual request construction code. This allows a compromised renderer to bypass the VerifyNavigationHeaders allowlist validation check and inject arbitrary HTTP request headers on browser-initiated cross-process navigations.

Root Cause

VerifyNavigationHeaders (located in content/browser/renderer_host/ipc_utils.cc) validates the renderer-supplied OpenURLParams::extra_headers string before it undergoes linefeed normalization (from LF to CRLF). However, the actual outbound request headers are built from the normalized CRLF string.

Both parsing passes utilize net::HttpRequestHeaders::AddHeadersFromString, which splits lines using only the literal substring "\r\n" as a delimiter:

void HttpRequestHeaders::AddHeadersFromString(std::string_view headers) {
  for (std::string_view header : base::SplitStringPieceUsingSubstr(
           headers, "\r\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
    AddHeaderFromString(header);
  }
}

1. Validation Pass

When checking a raw string containing only bare linefeed separators (e.g., "X\nX-CSRF-Token: bypass\nAuthorization: Bearer evil"), there is no "\r\n" substring. Consequently, the entire string is processed as a single header line.

Inside AddHeaderFromString, HttpUtil::IsValidHeaderName determines that the derived key ("X\nX-CSRF-Token") contains an invalid token character (\n) and rejects the entire line, leaving parsed_headers completely empty. Since no headers are extracted, the allowlist loop in VerifyNavigationHeaders executes zero times, validation succeeds, and the renderer is not terminated.

2. Normalization and Usage Pass

After successfully bypassing validation, the string is normalized in NavigationControllerImpl::CreateNavigationRequestFromLoadParams:

std::string extra_headers_crlf;
base::ReplaceChars(params.extra_headers, "\n", "\r\n", &extra_headers_crlf);

This expands the linefeeds to CRLF, resulting in: "X\r\nX-CSRF-Token: bypass\r\nAuthorization: Bearer evil".

When NavigationRequest::Create eventually parses this normalized string via AddHeadersFromString, it successfully splits on "\r\n" and extracts multiple distinct headers. The dummy first header "X" is dropped because it contains no colon, but all subsequent headers (such as X-CSRF-Token and Authorization) are successfully registered and sent on the outbound request.

Potential Trigger Path

  1. A compromised renderer invokes the Mojo endpoint RemoteFrameHost::OpenURL on a cross-process target frame, supplying extra_headers structured with bare \n linefeeds containing forbidden headers.
  2. RenderFrameProxyHost::OpenURL calls VerifyOpenURLParams, which validates the raw, non-normalized string via VerifyNavigationHeaders. Due to the bare linefeeds, no headers are parsed, validation returns true, and the renderer survives.
  3. NavigateFromFrameProxy forwards the raw parameters to NavigationControllerImpl::CreateNavigationRequestFromLoadParams, which normalizes the linefeeds into CRLF via base::ReplaceChars.
  4. NavigationRequest parses the normalized string, registering the injected headers.
  5. The browser issues the network navigation request carrying the forged headers.

(Note: These are potential steps based on static analysis of the codebase, as we have not executed runtime proof-of-concept tests).

Suggested Fix

To remediate this issue, the browser should perform linefeed normalization to CRLF before running validation in VerifyNavigationHeaders, or explicitly reject any renderer-supplied extra_headers string that contains bare \n (without \r) before any processing is done.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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