blob: 3cc78182588273ed8dcfc0132028cedfb72cdacc [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/user_script_loader.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/mock_render_process_host.h"
#include "extensions/browser/embedder_user_script_loader.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
#include "extensions/browser/renderer_startup_helper.h"
#include "extensions/common/mojom/host_id.mojom.h"
#include "extensions/common/mojom/renderer.mojom.h"
#include "extensions/common/user_script.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace extensions {
namespace {
// A RendererStartupHelper that intercepts mojom::Renderer messages and records
// which processes receive UpdateUserScripts. This lets the tests below verify
// that user scripts are only shipped to the intended set of renderers.
class TestRendererStartupHelper : public RendererStartupHelper,
public mojom::Renderer {
public:
explicit TestRendererStartupHelper(content::BrowserContext* browser_context)
: RendererStartupHelper(browser_context) {}
static std::unique_ptr<KeyedService> Build(
content::BrowserContext* browser_context) {
return std::make_unique<TestRendererStartupHelper>(browser_context);
}
bool ProcessReceivedUpdateUserScripts(
content::RenderProcessHost* process) const {
return updated_processes_.contains(process);
}
protected:
// RendererStartupHelper:
mojo::PendingAssociatedRemote<mojom::Renderer> BindNewRendererRemote(
content::RenderProcessHost* process) override {
mojo::AssociatedRemote<mojom::Renderer> remote;
renderer_receivers_.Add(
this, remote.BindNewEndpointAndPassDedicatedReceiver(), process);
return remote.Unbind();
}
private:
// mojom::Renderer:
void ActivateExtension(const ExtensionId& extension_id) override {}
void SetActivityLoggingEnabled(bool enabled) override {}
void SetPolicyActivityLoggingEnabled(bool enabled) override {}
void LoadExtensions(
std::vector<mojom::ExtensionLoadedParamsPtr> loaded_extensions) override {
}
void UnloadExtension(const ExtensionId& extension_id) override {}
void SuspendExtension(
const ExtensionId& extension_id,
mojom::Renderer::SuspendExtensionCallback callback) override {
std::move(callback).Run();
}
void CancelSuspendExtension(const ExtensionId& extension_id) override {}
void SetDeveloperMode(bool current_developer_mode) override {}
void SetUserScriptsAllowed(const ExtensionId& extension_id,
bool allowed) override {}
void SetSessionInfo(version_info::Channel channel,
mojom::FeatureSessionType session) override {}
void SetSystemFont(const std::string& font_family,
const std::string& font_size) override {}
void SetWebViewPartitionID(const std::string& partition_id) override {}
void SetScriptingAllowlist(
const std::vector<ExtensionId>& extension_ids) override {}
void UpdateUserScriptWorlds(
std::vector<mojom::UserScriptWorldInfoPtr> info) override {}
void ClearUserScriptWorldConfig(
const ExtensionId& extension_id,
const std::optional<std::string>& world_id) override {}
void ShouldSuspend(ShouldSuspendCallback callback) override {
std::move(callback).Run();
}
void TransferBlobs(TransferBlobsCallback callback) override {
std::move(callback).Run();
}
void UpdatePermissions(const ExtensionId& extension_id,
PermissionSet active_permissions,
PermissionSet withheld_permissions,
URLPatternSet policy_blocked_hosts,
URLPatternSet policy_allowed_hosts,
bool uses_default_policy_host_restrictions) override {}
void UpdateDefaultPolicyHostRestrictions(
URLPatternSet default_policy_blocked_hosts,
URLPatternSet default_policy_allowed_hosts) override {}
void UpdateUserHostRestrictions(URLPatternSet user_blocked_hosts,
URLPatternSet user_allowed_hosts) override {}
void UpdateTabSpecificPermissions(const ExtensionId& extension_id,
URLPatternSet new_hosts,
int tab_id,
bool update_origin_allowlist) override {}
void UpdateUserScripts(base::ReadOnlySharedMemoryRegion shared_memory,
mojom::HostIDPtr host_id) override {
updated_processes_.insert(renderer_receivers_.current_context());
}
void ClearTabSpecificPermissions(
const std::vector<ExtensionId>& extension_ids,
int tab_id,
bool update_origin_allowlist) override {}
void WatchPages(const std::vector<std::string>& css_selectors) override {}
std::set<content::RenderProcessHost*> updated_processes_;
mojo::AssociatedReceiverSet<mojom::Renderer, content::RenderProcessHost*>
renderer_receivers_;
};
class UserScriptLoaderUnitTest : public ExtensionsTest {
public:
UserScriptLoaderUnitTest() = default;
UserScriptLoaderUnitTest(const UserScriptLoaderUnitTest&) = delete;
UserScriptLoaderUnitTest& operator=(const UserScriptLoaderUnitTest&) = delete;
~UserScriptLoaderUnitTest() override = default;
void SetUp() override {
ExtensionsTest::SetUp();
helper_ = static_cast<TestRendererStartupHelper*>(
RendererStartupHelperFactory::GetInstance()->SetTestingFactoryAndUse(
browser_context(),
base::BindRepeating(&TestRendererStartupHelper::Build)));
}
void TearDown() override {
helper_ = nullptr;
ExtensionsTest::TearDown();
}
protected:
TestRendererStartupHelper* helper() { return helper_; }
std::unique_ptr<content::MockRenderProcessHost> CreateAndInitializeProcess(
bool is_for_guests_only) {
auto process = std::make_unique<content::MockRenderProcessHost>(
browser_context(), is_for_guests_only);
helper_->OnRenderProcessHostCreated(process.get());
if (is_for_guests_only) {
helper_->OnRenderProcessLaunched(process.get());
}
return process;
}
void LoadScriptsAndWait(UserScriptLoader* loader,
content::RenderProcessHost* embedder_process) {
auto script = std::make_unique<UserScript>();
script->set_id(UserScript::GenerateUserScriptID());
script->set_host_id(loader->host_id());
auto content = UserScript::Content::CreateInlineCode(
GURL("https://embedder.example/inline.js"));
content->set_content("/* content script body */");
script->js_scripts().push_back(std::move(content));
UserScriptList scripts;
scripts.push_back(std::move(script));
base::RunLoop run_loop;
loader->AddScripts(
std::move(scripts), embedder_process->GetDeprecatedID(),
/*render_frame_id=*/0,
base::BindLambdaForTesting(
[&run_loop](UserScriptLoader*,
const std::optional<std::string>& error) {
EXPECT_FALSE(error.has_value()) << *error;
run_loop.Quit();
}));
run_loop.Run();
// Flush any pending mojo messages so that UpdateUserScripts is delivered.
helper_->FlushAllForTesting();
base::RunLoop().RunUntilIdle();
}
private:
raw_ptr<TestRendererStartupHelper> helper_ = nullptr;
};
// Test success case that embedder content script is sent to
// guest renderer.
TEST_F(UserScriptLoaderUnitTest, EmbedderScriptsSentToGuestRenderer) {
std::unique_ptr<content::MockRenderProcessHost> guest_process =
CreateAndInitializeProcess(/*is_for_guests_only=*/true);
ASSERT_TRUE(helper()->IsProcessInitializedForTesting(guest_process.get()));
const std::string owner_host = "isolated-app://embedder.example";
WebViewRendererState::WebViewInfo web_view_info;
web_view_info.owner_host = owner_host;
const int dummy_routing_id = 1;
WebViewRendererState::GetInstance()->AddGuestForTesting(
guest_process->GetDeprecatedID(), dummy_routing_id, web_view_info);
base::ScopedClosureRunner cleanup_guest(base::BindOnce(
[](int process_id, int routing_id) {
WebViewRendererState::GetInstance()->RemoveGuestForTesting(process_id,
routing_id);
},
guest_process->GetDeprecatedID(), dummy_routing_id));
EmbedderUserScriptLoader loader(
browser_context(),
mojom::HostID(mojom::HostID::HostType::kControlledFrameEmbedder,
owner_host));
LoadScriptsAndWait(&loader, guest_process.get());
EXPECT_TRUE(loader.initial_load_complete());
EXPECT_TRUE(helper()->ProcessReceivedUpdateUserScripts(guest_process.get()));
}
// Content scripts that an embedder adds to its own guests
// only ever inject into those guests, so they should not be shipped to
// unrelated renderer processes hosting ordinary web content in the same
// profile.
TEST_F(UserScriptLoaderUnitTest, EmbedderScriptsNotSentToNonGuestRenderer) {
const struct {
mojom::HostID::HostType host_type;
const char* host;
} test_cases[] = {
{mojom::HostID::HostType::kControlledFrameEmbedder,
"isolated-app://embedder.example"},
{mojom::HostID::HostType::kWebUi, "chrome://embedder.example/"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message() << "host=" << test_case.host);
std::unique_ptr<content::MockRenderProcessHost> web_process =
CreateAndInitializeProcess(/*is_for_guests_only=*/false);
ASSERT_TRUE(helper()->IsProcessInitializedForTesting(web_process.get()));
EmbedderUserScriptLoader loader(
browser_context(), mojom::HostID(test_case.host_type, test_case.host));
LoadScriptsAndWait(&loader, web_process.get());
EXPECT_TRUE(loader.initial_load_complete());
EXPECT_FALSE(helper()->ProcessReceivedUpdateUserScripts(web_process.get()));
}
}
} // namespace
} // namespace extensions