CVE-2026-113035
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifextensions/browser/api/messaging/message_service_bindings.cc |
modified | |
ifchrome/browser/extensions/extension_security_exploit_browsertest.cc |
modified | |
BindLambdaForTestingchrome/browser/extensions/extension_security_exploit_browsertest.cc |
modified |
Files Changed
chrome/browser/extensions/extension_security_exploit_browsertest.cctools/metrics/histograms/metadata/stability/enums.xmlextensions/browser/api/messaging/message_service_bindings.ccextensions/browser/bad_message.h
Patch
From 7b0f66cf84e3a7f485819c6a9b81e6b8e95f1dbf Mon Sep 17 00:00:00 2001
From: Justin Lulejian <jlulejian@chromium.org>
Date: Fri, 18 Sep 2026 14:38:32 -0700
Subject: [PATCH] [Extensions] Validate channel type in OpenChannelToExtension and Tab
In extension messaging, native messaging channels are established
internally by the browser process and are not intended to be initiated
directly by renderer processes. Previously, MessageService did not
validate the channel_type argument in OpenChannelToExtension and
OpenChannelToTab IPC requests from renderers. If a renderer specified
mojom::ChannelType::kNative, the unexpected channel type would be
forwarded to the target extension.
After this commit lands, incoming OpenChannelToExtension and
OpenChannelToTab IPC requests that specify mojom::ChannelType::kNative
are rejected as invalid messaging requests, terminating the calling
renderer with bad_message::EMF_INVALID_CHANNEL_TYPE.
This is accomplished by adding an explicit channel_type validation check
to MessageService::OpenChannelToExtension and OpenChannelToTab in
extensions/browser/api/messaging/message_service_bindings.cc, updating
test helper interceptor signatures, and adding a regression test.
Fixed: 498786597
Change-Id: I57da6aa6a509b4f914a53d4b5bbd1640514a088f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8391877
Auto-Submit: Justin Lulejian <jlulejian@chromium.org>
Commit-Queue: Justin Lulejian <jlulejian@chromium.org>
Reviewed-by: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Tim <tjudkins@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701096}
---
diff --git a/extensions/browser/api/messaging/message_service_bindings.cc b/extensions/browser/api/messaging/message_service_bindings.cc
index a3ad6ba..bb0f518 100644
--- a/extensions/browser/api/messaging/message_service_bindings.cc
+++ b/extensions/browser/api/messaging/message_service_bindings.cc
@@ -476,6 +476,11 @@
ScopedExternalConnectionInfoCrashKeys info_crash_keys(info);
debug::ScopedPortContextCrashKeys port_context_crash_keys(
source.port_context());
+ if (channel_type == mojom::ChannelType::kNative) {
+ bad_message::ReceivedBadMessage(process,
+ bad_message::EMF_INVALID_CHANNEL_TYPE);
+ return;
+ }
if (!IsValidMessagingSource(*process, info.source_endpoint.type,
info.source_endpoint.extension_id,
source.port_context()) ||
@@ -581,6 +586,13 @@
if (!process) {
return;
}
+ debug::ScopedPortContextCrashKeys port_context_crash_keys(
+ source.port_context());
+ if (channel_type == mojom::ChannelType::kNative) {
+ bad_message::ReceivedBadMessage(process,
+ bad_message::EMF_INVALID_CHANNEL_TYPE);
+ return;
+ }
std::optional<ExtensionId> extension_id =
ValidateSourceContextAndExtractExtensionId(*process,
source.port_context());
diff --git a/extensions/browser/bad_message.h b/extensions/browser/bad_message.h
index 1856258..349bf333e 100644
--- a/extensions/browser/bad_message.h
+++ b/extensions/browser/bad_message.h
@@ -82,6 +82,7 @@
WER_INVALID_EXTENSION_ID = 42,
AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING = 43,
MS_EMPTY_ANDROID_CERTIFICATES_FOR_PACKED_EXTENSION = 44,
+ EMF_INVALID_CHANNEL_TYPE = 45,
// Please add new elements here. The naming convention is abbreviated class
// name (e.g. ExtensionHost becomes EH) plus a unique description of the
// reason. After making changes, you MUST update histograms.xml by running:
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index 73531fc..21c1549a 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -643,6 +643,7 @@
<int value="42" label="WER_INVALID_EXTENSION_ID"/>
<int value="43" label="AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING"/>
<int value="44" label="MS_EMPTY_ANDROID_CERTIFICATES_FOR_PACKED_EXTENSION"/>
+ <int value="45" label="EMF_INVALID_CHANNEL_TYPE"/>
</enum>
<enum name="BadMessageReasonGuestView">
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index e1637eb..ef151b43 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -152,7 +152,8 @@
}
using OpenChannelToExtensionMutator =
- base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
+ base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type)>;
void SetOpenChannelToExtensionMutator(
OpenChannelToExtensionMutator open_extension_mutator) {
open_extension_mutator_ = std::move(open_extension_mutator);
@@ -235,7 +236,7 @@
content::RenderFrameHost* current_target_frame =
&extension_frame_host_->receivers_for_testing().CurrentTargetFrame();
if (open_extension_mutator_ && frame_ == current_target_frame) {
- open_extension_mutator_.Run(*info);
+ open_extension_mutator_.Run(*info, channel_type);
}
GetForwardingInterface()->OpenChannelToExtension(
@@ -296,7 +297,7 @@
override {
CHECK(info);
if (open_extension_mutator_) {
- open_extension_mutator_.Run(*info);
+ open_extension_mutator_.Run(*info, channel_type);
}
GetForwardingInterface()->OpenChannelToExtension(
@@ -305,7 +306,8 @@
}
using OpenChannelToExtensionMutator =
- base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
+ base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type)>;
void SetOpenChannelToExtensionMutator(
OpenChannelToExtensionMutator open_extension_mutator) {
open_extension_mutator_ = std::move(open_extension_mutator);
@@ -552,7 +554,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -563,6 +566,44 @@
kill_waiter.Wait());
}
+// Tests that a renderer process attempting to establish a messaging channel
+// with an unexpected `mojom::ChannelType::kNative` in
+// `OpenChannelToExtension` is terminated.
+IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
+ FromContentScript_UnexpectedNativeChannelType) {
+ // Trigger sending of a valid `OpenChannelToExtension` IPC from a content
+ // script of `active_extension_id()`.
+ auto* web_contents = GetActiveWebContents();
+ ASSERT_TRUE(ExecuteProgrammaticContentScript(
+ web_contents, active_extension_id(),
+ /*content_script=*/
+ "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
+
+ // Set up a process kill waiter on the renderer process.
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+ // Intercept the outgoing IPC from `ExtensionFrameHost` and mutate the
+ // channel type to `mojom::ChannelType::kNative`.
+ auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
+ web_contents->GetPrimaryMainFrame());
+ interceptor->SetOpenChannelToExtensionMutator(
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
+ EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
+ info.source_endpoint.type);
+ EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
+
+ // Mutate the IPC channel type to `mojom::ChannelType::kNative`.
+ channel_type = mojom::ChannelType::kNative;
+ }));
+
+ // Verify that the browser terminates the renderer with
+ // `bad_message::EMF_INVALID_CHANNEL_TYPE`.
+ EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_TYPE, kill_waiter.Wait());
+}
+
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromContentScript_UnexpectedNativeAppType) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
@@ -578,7 +619,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -603,7 +645,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -629,7 +672,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -657,7 +701,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -703,7 +748,8 @@
std::make_unique<ServiceWorkerHostInterceptorForProcessDeath>(
service_workers[0]);
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kExtension,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -829,6 +875,47 @@
kill_waiter.Wait());
}
+// Tests that an extension frame attempting to connect to a tab with an
+// unexpected `mojom::ChannelType::kNative` in
+// `MessageService::OpenChannelToTab` is rejected and the renderer process is
+// terminated.
+IN_PROC_BROWSER_TEST_F(
+ OpenChannelToExtensionExploitTest,
+ FromExtensionFrame_UnexpectedNativeChannelType_OpenChannelToTab) {
+ // Navigate the test tab to an extension page.
+ GURL test_page_url = active_extension().GetResourceURL("page.html");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), test_page_url));
+
+ // Set up a process kill waiter on the renderer process.
+ content::RenderFrameHost* main_frame =
+ GetActiveWebContents()->GetPrimaryMainFrame();
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
+
+ // Create message port endpoints for the channel.
+ mojo::PendingAssociatedRemote<mojom::MessagePort> port;
+ mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host;
+ std::ignore = port.InitWithNewEndpointAndPassReceiver();
+ std::ignore = port_host.InitWithNewEndpointAndPassRemote();
+
+ // Send an `OpenChannelToTab` request specifying
+ // `mojom::ChannelType::kNative`.
+ content::RenderProcessHost* process = main_frame->GetProcess();
+ ChannelEndpoint source(profile(), process->GetDeprecatedID(),
+ PortContext::ForFrame(main_frame->GetRoutingID()));
+
+ MessageService::Get(profile())->OpenChannelToTab(
+ source,
+ PortId(base::UnguessableToken::Create(), /*port_number=*/0,
+ /*is_opener=*/true, mojom::SerializationFormat::kJson),
+ /*tab_id=*/1, /*frame_id=*/-1, /*document_id=*/std::string(),
+ mojom::ChannelType::kNative, /*channel_name=*/"test_channel",
+ std::move(port), std::move(port_host));
+
+ // Verify that the browser terminates the renderer process with
+ // `bad_message::EMF_INVALID_CHANNEL_TYPE`.
+ EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_TYPE, kill_waiter.Wait());
+}
+
// This is a regression test for https://crbug.com/40875650.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
SendMessageFromContentScriptInDataUrlFrame) {
@@ -972,7 +1059,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -1000,7 +1088,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -1028,7 +1117,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
Regression Test / PoC
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index e1637eb..ef151b43 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -152,7 +152,8 @@
}
using OpenChannelToExtensionMutator =
- base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
+ base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type)>;
void SetOpenChannelToExtensionMutator(
OpenChannelToExtensionMutator open_extension_mutator) {
open_extension_mutator_ = std::move(open_extension_mutator);
@@ -235,7 +236,7 @@
content::RenderFrameHost* current_target_frame =
&extension_frame_host_->receivers_for_testing().CurrentTargetFrame();
if (open_extension_mutator_ && frame_ == current_target_frame) {
- open_extension_mutator_.Run(*info);
+ open_extension_mutator_.Run(*info, channel_type);
}
GetForwardingInterface()->OpenChannelToExtension(
@@ -296,7 +297,7 @@
override {
CHECK(info);
if (open_extension_mutator_) {
- open_extension_mutator_.Run(*info);
+ open_extension_mutator_.Run(*info, channel_type);
}
GetForwardingInterface()->OpenChannelToExtension(
@@ -305,7 +306,8 @@
}
using OpenChannelToExtensionMutator =
- base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
+ base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type)>;
void SetOpenChannelToExtensionMutator(
OpenChannelToExtensionMutator open_extension_mutator) {
open_extension_mutator_ = std::move(open_extension_mutator);
@@ -552,7 +554,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -563,6 +566,44 @@
kill_waiter.Wait());
}
+// Tests that a renderer process attempting to establish a messaging channel
+// with an unexpected `mojom::ChannelType::kNative` in
+// `OpenChannelToExtension` is terminated.
+IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
+ FromContentScript_UnexpectedNativeChannelType) {
+ // Trigger sending of a valid `OpenChannelToExtension` IPC from a content
+ // script of `active_extension_id()`.
+ auto* web_contents = GetActiveWebContents();
+ ASSERT_TRUE(ExecuteProgrammaticContentScript(
+ web_contents, active_extension_id(),
+ /*content_script=*/
+ "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
+
+ // Set up a process kill waiter on the renderer process.
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+ // Intercept the outgoing IPC from `ExtensionFrameHost` and mutate the
+ // channel type to `mojom::ChannelType::kNative`.
+ auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
+ web_contents->GetPrimaryMainFrame());
+ interceptor->SetOpenChannelToExtensionMutator(
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
+ EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
+ info.source_endpoint.type);
+ EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
+
+ // Mutate the IPC channel type to `mojom::ChannelType::kNative`.
+ channel_type = mojom::ChannelType::kNative;
+ }));
+
+ // Verify that the browser terminates the renderer with
+ // `bad_message::EMF_INVALID_CHANNEL_TYPE`.
+ EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_TYPE, kill_waiter.Wait());
+}
+
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromContentScript_UnexpectedNativeAppType) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
@@ -578,7 +619,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -603,7 +645,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -629,7 +672,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -657,7 +701,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -703,7 +748,8 @@
std::make_unique<ServiceWorkerHostInterceptorForProcessDeath>(
service_workers[0]);
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kExtension,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -829,6 +875,47 @@
kill_waiter.Wait());
}
+// Tests that an extension frame attempting to connect to a tab with an
+// unexpected `mojom::ChannelType::kNative` in
+// `MessageService::OpenChannelToTab` is rejected and the renderer process is
+// terminated.
+IN_PROC_BROWSER_TEST_F(
+ OpenChannelToExtensionExploitTest,
+ FromExtensionFrame_UnexpectedNativeChannelType_OpenChannelToTab) {
+ // Navigate the test tab to an extension page.
+ GURL test_page_url = active_extension().GetResourceURL("page.html");
+ ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), test_page_url));
+
+ // Set up a process kill waiter on the renderer process.
+ content::RenderFrameHost* main_frame =
+ GetActiveWebContents()->GetPrimaryMainFrame();
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
+
+ // Create message port endpoints for the channel.
+ mojo::PendingAssociatedRemote<mojom::MessagePort> port;
+ mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host;
+ std::ignore = port.InitWithNewEndpointAndPassReceiver();
+ std::ignore = port_host.InitWithNewEndpointAndPassRemote();
+
+ // Send an `OpenChannelToTab` request specifying
+ // `mojom::ChannelType::kNative`.
+ content::RenderProcessHost* process = main_frame->GetProcess();
+ ChannelEndpoint source(profile(), process->GetDeprecatedID(),
+ PortContext::ForFrame(main_frame->GetRoutingID()));
+
+ MessageService::Get(profile())->OpenChannelToTab(
+ source,
+ PortId(base::UnguessableToken::Create(), /*port_number=*/0,
+ /*is_opener=*/true, mojom::SerializationFormat::kJson),
+ /*tab_id=*/1, /*frame_id=*/-1, /*document_id=*/std::string(),
+ mojom::ChannelType::kNative, /*channel_name=*/"test_channel",
+ std::move(port), std::move(port_host));
+
+ // Verify that the browser terminates the renderer process with
+ // `bad_message::EMF_INVALID_CHANNEL_TYPE`.
+ EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_TYPE, kill_waiter.Wait());
+}
+
// This is a regression test for https://crbug.com/40875650.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
SendMessageFromContentScriptInDataUrlFrame) {
@@ -972,7 +1059,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -1000,7 +1088,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
@@ -1028,7 +1117,8 @@
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
web_contents->GetPrimaryMainFrame());
interceptor->SetOpenChannelToExtensionMutator(
- base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
+ base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info,
+ mojom::ChannelType& channel_type) {
EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
Original Bug Report
Privilege escalation: Compromised renderer can spoof onConnectNative to extensions
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: A compromised renderer can spoof a connection from a native application to an extension by providing an unvalidated ChannelType::kNative in the OpenChannelToExtension IPC. This triggers the extension’s chrome.runtime.onConnectNative listener, bypassing the security boundary between web content and trusted native messaging.
Affected files:
extensions/browser/api/messaging/message_service_bindings.ccextensions/renderer/api/messaging/messaging_util.ccextensions/renderer/api/messaging/native_renderer_messaging_service.ccextensions/browser/api/messaging/message_service.ccextensions/browser/extension_frame_host.cc
Estimated timestamp from git blame: 2024-05-03
Description
The browser-process IPC validation layer in MessageService::OpenChannelToExtension (located in extensions/browser/api/messaging/message_service_bindings.cc) fails to validate the channel_type parameter provided by the renderer process.
While the code correctly validates info.source_endpoint via IsValidMessagingSource() (rejecting kNativeApp endpoint types originating from renderers), the channel_type parameter is passed through to the target extension without any checks.
A compromised renderer can exploit this by sending a LocalFrameHost::OpenChannelToExtension (or ServiceWorkerHost::OpenChannelToExtension) IPC with a legitimate source_endpoint.type (e.g., kWebPage) but with channel_type set to mojom::ChannelType::kNative. This unvalidated channel_type flows through MessageService::OpenChannelToExtension in the browser process and finally to the target extension’s renderer process.
On the receiving side, messaging_util::GetEventForChannel (in extensions/renderer/api/messaging/messaging_util.cc) determines the Javascript event to fire based strictly on the channel_type. For kNative, it unconditionally returns kOnConnectNativeEvent. Consequently, the target extension’s chrome.runtime.onConnectNative listener is triggered.
Impact
The chrome.runtime.onConnectNative event is architecturally reserved for connections originating from trusted, local native messaging binaries. Extensions that implement onConnectNative handlers (such as enterprise agents, password managers, or hardware authenticators) typically treat the peer as authenticated and highly trusted.
A compromised renderer for an origin allowed by an extension’s externally_connectable.matches can spoof this identity. While the port.sender object will lack the nativeApplication field, many extensions do not explicitly check for its presence, relying instead on the fact that the onConnectNative event fired at all.
This violates the extension security model, resulting in a potential privilege escalation and sandbox escape depending on the capabilities exposed by the extension’s native messaging interface.
Potential Reproduction Steps
Note: These are suggested steps; we have not yet run a working proof of concept.
- Identify a target extension with the
nativeMessagingpermission that implements achrome.runtime.onConnectNativelistener. - Ensure the extension allows external connections from a specific web origin via
externally_connectablein its manifest. - From a compromised renderer hosting that origin, acquire the
extensions.mojom.LocalFrameHostMojo interface. - Invoke the
OpenChannelToExtensionIPC with the following parameters:info.source_endpoint.type=MessagingEndpoint::Type::kWebPageinfo.target_id=<target_extension_id>channel_type=mojom::ChannelType::kNative
- Observe that the target extension receives an
onConnectNativeevent, even though the connection originated from a web page renderer.
Recommended Fix
Add validation in MessageService::OpenChannelToExtension (in extensions/browser/api/messaging/message_service_bindings.cc) to reject requests where channel_type == mojom::ChannelType::kNative. Connections from legitimate native applications do not originate from renderer processes and use a different initialization path (OpenChannelToNativeApp or are initiated by the browser process directly). If channel_type == mojom::ChannelType::kNative is observed arriving via this renderer-facing IPC, the browser should call bad_message::ReceivedBadMessage to terminate the process.
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.