blob: 2dca521dfaa2e9b56fbdbe7190de32f20b5fb394 [file] [log] [blame]
// Copyright 2014 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/extension_protocols.h"
#include <stddef.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/power_monitor_test.h"
#include "base/test/test_file_util.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_content_verifier_delegate.h"
#include "chrome/browser/extensions/chrome_extensions_browser_client.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_profile.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/content_verifier/content_verifier.h"
#include "extensions/browser/content_verifier/test_utils.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/file_util.h"
#include "extensions/test/test_extension_dir.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/loader/referrer_utils.h"
using extensions::ExtensionRegistry;
using network::mojom::URLLoader;
using testing::_;
using testing::StrictMock;
namespace extensions {
namespace {
constexpr char kValidTrialToken1[] = "valid_token_1";
constexpr char kValidTrialToken2[] = "valid_token_2";
constexpr char kTrialTokensHeaderValue[] = "valid_token_1, valid_token_2";
base::FilePath GetTestPath(const std::string& name) {
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
return path.AppendASCII("extensions").AppendASCII(name);
}
base::FilePath GetContentVerifierTestPath() {
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &path));
return path.AppendASCII("content_hash_fetcher")
.AppendASCII("different_sized_files");
}
scoped_refptr<const Extension> CreateTestExtension(const std::string& name,
bool incognito_split_mode,
int manifest_version) {
return ExtensionBuilder(name)
.SetManifestVersion(manifest_version)
.SetManifestKey("incognito", incognito_split_mode ? "split" : "spanning")
.SetPath(GetTestPath("response_headers"))
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
}
scoped_refptr<const Extension> CreateWebStoreExtension(int manifest_version) {
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(chrome::DIR_RESOURCES, &path));
path = path.AppendASCII("web_store");
return ExtensionBuilder("WebStore")
.SetManifestVersion(manifest_version)
.SetManifestKey("icons",
base::Value::Dict().Set("16", "webstore_icon_16.png"))
.SetManifestKey(
"web_accessible_resources",
manifest_version == 3
? base::Value::List().Append(
base::Value::Dict()
.Set("resources",
base::Value::List().Append("webstore_icon_16.png"))
.Set("matches", base::Value::List().Append("*://*/*")))
: base::Value::List().Append("webstore_icon_16.png"))
.SetPath(path)
.SetLocation(mojom::ManifestLocation::kComponent)
.Build();
}
scoped_refptr<const Extension> CreateTestResponseHeaderExtension(
int manifest_version) {
if (manifest_version == 3) {
return ExtensionBuilder("An extension with web-accessible resources")
.SetManifestVersion(3)
.SetManifestKey(
"web_accessible_resources",
base::Value::List().Append(
base::Value::Dict()
.Set("resources",
base::Value::List()
.Append("test.dat")
.Append("mime_type_sniffer_test.gif1"))
.Set("matches", base::Value::List().Append("*://*/*"))))
.SetManifestKey("background", base::Value::Dict().Set("service_worker",
"background.js"))
.SetManifestKey("trial_tokens", base::Value::List()
.Append(kValidTrialToken1)
.Append(kValidTrialToken2))
.SetPath(GetTestPath("response_headers"))
.Build();
}
return ExtensionBuilder("An extension with web-accessible resources")
.SetManifestVersion(manifest_version)
.SetManifestKey("web_accessible_resources",
base::Value::List()
.Append("test.dat")
.Append("mime_type_sniffer_test.gif1"))
.SetManifestKey(
"background",
base::Value::Dict().Set("scripts",
base::Value::List().Append("background.js")))
.SetPath(GetTestPath("response_headers"))
.Build();
}
scoped_refptr<const Extension> CreateTestModuleResponseHeaderExtension(
int manifest_version) {
return ExtensionBuilder("A module extension")
.SetManifestVersion(manifest_version)
.SetManifestKey("export", base::Value::Dict())
.SetPath(GetTestPath("response_headers"))
.Build();
}
scoped_refptr<const Extension> CreateTestModuleImporterResponseHeaderExtension(
int manifest_version,
const std::string& module_extension_id) {
if (manifest_version == 3) {
return ExtensionBuilder("A module importer extension")
.SetManifestVersion(3)
.SetManifestKey("import",
base::Value::List().Append(
base::Value::Dict().Set("id", module_extension_id)))
.SetManifestKey("trial_tokens", base::Value::List()
.Append(kValidTrialToken1)
.Append(kValidTrialToken2))
.SetPath(GetTestPath("response_headers"))
.Build();
}
return ExtensionBuilder("A module importer extension")
.SetManifestVersion(manifest_version)
.SetManifestKey("import",
base::Value::List().Append(
base::Value::Dict().Set("id", module_extension_id)))
.SetPath(GetTestPath("response_headers"))
.Build();
}
// Helper function to create a |ResourceRequest| for testing purposes.
network::ResourceRequest CreateResourceRequest(
const std::string& method,
network::mojom::RequestDestination destination,
const GURL& url) {
network::ResourceRequest request;
request.method = method;
request.url = url;
request.site_for_cookies =
net::SiteForCookies::FromUrl(url); // bypass third-party cookie blocking.
request.request_initiator =
url::Origin::Create(url); // ensure initiator set.
request.referrer_policy = blink::ReferrerUtils::GetDefaultNetReferrerPolicy();
request.destination = destination;
request.is_outermost_main_frame =
destination == network::mojom::RequestDestination::kDocument;
return request;
}
// The result of either a URLRequest of a URLLoader response (but not both)
// depending on the on test type.
class GetResult {
public:
GetResult(network::mojom::URLResponseHeadPtr response, int result)
: response_(std::move(response)), result_(result) {}
GetResult(GetResult&& other) : result_(other.result_) {}
GetResult(const GetResult&) = delete;
GetResult& operator=(const GetResult&) = delete;
~GetResult() = default;
std::string GetResponseHeaderByName(const std::string& name) const {
if (!response_ || !response_->headers) {
return std::string();
}
return response_->headers->GetNormalizedHeader(name).value_or(
std::string());
}
bool HasContentLengthHeader() {
std::string content_length =
GetResponseHeaderByName(net::HttpRequestHeaders::kContentLength);
int length_value = 0;
return !content_length.empty() &&
base::StringToInt(content_length, &length_value) && length_value > 0;
}
bool HeaderIsPresent(const std::string& name) {
return !GetResponseHeaderByName(name).empty();
}
int result() const { return result_; }
private:
network::mojom::URLResponseHeadPtr response_;
int result_;
};
} // namespace
// This test lives in src/chrome instead of src/extensions because it tests
// functionality delegated back to Chrome via ChromeExtensionsBrowserClient.
// See chrome/browser/extensions/chrome_url_request_util.cc.
class ExtensionProtocolsTestBase : public testing::Test,
public testing::WithParamInterface<int> {
public:
explicit ExtensionProtocolsTestBase(bool force_incognito)
: task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
rvh_test_enabler_(new content::RenderViewHostTestEnabler()),
force_incognito_(force_incognito) {}
void SetUp() override {
testing::Test::SetUp();
testing_profile_ = TestingProfile::Builder().Build();
contents_ = CreateTestWebContents();
// Set up content verification.
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
command_line->AppendSwitchASCII(
switches::kExtensionContentVerification,
switches::kExtensionContentVerificationEnforce);
content_verifier_ = new ContentVerifier(
browser_context(),
std::make_unique<ChromeContentVerifierDelegate>(browser_context()));
content_verifier_->Start();
static_cast<TestExtensionSystem*>(ExtensionSystem::Get(browser_context()))
->set_content_verifier(content_verifier_.get());
loader_factory_.Bind(
CreateExtensionNavigationURLLoaderFactory(browser_context(), false));
}
void TearDown() override {
loader_factory_.reset();
content_verifier_->Shutdown();
// Shut down the PowerMonitor if initialized.
base::PowerMonitor::GetInstance()->ShutdownForTesting();
}
GetResult RequestOrLoad(const GURL& url,
network::mojom::RequestDestination destination) {
return LoadURL(url, destination);
}
void AddExtension(const scoped_refptr<const Extension>& extension,
bool incognito_enabled,
bool notifications_disabled) {
EXPECT_TRUE(extension_registry()->AddEnabled(extension));
extension_registry()->TriggerOnLoaded(extension.get());
ExtensionPrefs::Get(browser_context())
->SetIsIncognitoEnabled(extension->id(), incognito_enabled);
}
void RemoveExtension(const scoped_refptr<const Extension>& extension,
const UnloadedExtensionReason reason) {
EXPECT_TRUE(extension_registry()->RemoveEnabled(extension->id()));
extension_registry()->TriggerOnUnloaded(extension.get(), reason);
if (reason == UnloadedExtensionReason::DISABLE)
EXPECT_TRUE(extension_registry()->AddDisabled(extension));
}
// Helper method to create a URL request/loader, call RequestOrLoad on it, and
// return the result. If |extension| hasn't already been added to
// extension_registry(), this will add it.
GetResult DoRequestOrLoad(const scoped_refptr<Extension> extension,
const std::string& relative_path) {
if (!extension_registry()->enabled_extensions().Contains(extension->id())) {
AddExtension(extension.get(),
/*incognito_enabled=*/false,
/*notifications_disabled=*/false);
}
return RequestOrLoad(extension->ResolveExtensionURL(relative_path),
network::mojom::RequestDestination::kDocument);
}
ExtensionRegistry* extension_registry() {
return ExtensionRegistry::Get(browser_context());
}
content::BrowserContext* browser_context() {
return force_incognito_ ? testing_profile_->GetPrimaryOTRProfile(
/*create_if_needed=*/true)
: testing_profile_.get();
}
void EnableSimulationOfSystemSuspendForRequests() {
power_monitor_source_.emplace();
}
protected:
scoped_refptr<ContentVerifier> content_verifier_;
private:
GetResult LoadURL(const GURL& url,
network::mojom::RequestDestination destination) {
constexpr int32_t kRequestId = 28;
mojo::PendingRemote<network::mojom::URLLoader> loader;
network::TestURLLoaderClient client;
loader_factory_->CreateLoaderAndStart(
loader.InitWithNewPipeAndPassReceiver(), kRequestId,
network::mojom::kURLLoadOptionNone,
CreateResourceRequest("GET", destination, url), client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
// If `power_monitor_source_` is set, simulates power suspend and resume
// notifications. These notifications are posted tasks that will be executed
// by `client.RunUntilComplete()`.
if (power_monitor_source_) {
power_monitor_source_->Suspend();
power_monitor_source_->Resume();
}
client.RunUntilComplete();
return GetResult(client.response_head().Clone(),
client.completion_status().error_code);
}
std::unique_ptr<content::WebContents> CreateTestWebContents() {
auto site_instance = content::SiteInstance::Create(browser_context());
return content::WebContentsTester::CreateTestWebContents(
browser_context(), std::move(site_instance));
}
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
mojo::Remote<network::mojom::URLLoaderFactory> loader_factory_;
std::unique_ptr<TestingProfile> testing_profile_;
std::unique_ptr<content::WebContents> contents_;
const bool force_incognito_;
std::optional<base::test::ScopedPowerMonitorTestSource> power_monitor_source_;
};
class ExtensionProtocolsTest : public ExtensionProtocolsTestBase {
public:
ExtensionProtocolsTest()
: ExtensionProtocolsTestBase(false /*force_incognito*/) {}
};
class ExtensionProtocolsIncognitoTest : public ExtensionProtocolsTestBase {
public:
ExtensionProtocolsIncognitoTest()
: ExtensionProtocolsTestBase(true /*force_incognito*/) {}
};
// A specialization that will only run on MV3 extensions.
using ExtensionProtocolsMV3Test = ExtensionProtocolsTest;
INSTANTIATE_TEST_SUITE_P(MV2, ExtensionProtocolsTest, ::testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3, ExtensionProtocolsTest, ::testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV2,
ExtensionProtocolsIncognitoTest,
::testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3,
ExtensionProtocolsIncognitoTest,
::testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV3, ExtensionProtocolsMV3Test, ::testing::Values(3));
// Tests that making a chrome-extension request in an incognito context is
// only allowed under the right circumstances (if the extension is allowed
// in incognito, and it's either a non-main-frame request or a split-mode
// extension).
TEST_P(ExtensionProtocolsIncognitoTest, IncognitoRequest) {
struct TestCase {
// Inputs.
std::string name;
bool incognito_split_mode;
bool incognito_enabled;
// Expected result.
bool should_allow_main_frame_load;
} test_cases[] = {
{"spanning disabled", false, false, false},
{"split disabled", true, false, false},
{"spanning enabled", false, true, false},
{"split enabled", true, true, true},
};
for (const auto& test_case : test_cases) {
scoped_refptr<const Extension> extension = CreateTestExtension(
test_case.name, test_case.incognito_split_mode, GetParam());
AddExtension(extension, test_case.incognito_enabled, false);
// First test a main frame request.
// It doesn't matter that the resource doesn't exist. If the resource
// is blocked, we should see BLOCKED_BY_CLIENT. Otherwise, the request
// should just fail because the file doesn't exist.
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("404.html"),
network::mojom::RequestDestination::kDocument);
if (test_case.should_allow_main_frame_load) {
EXPECT_EQ(net::ERR_FILE_NOT_FOUND, get_result.result()) << test_case.name;
} else {
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result())
<< test_case.name;
}
// Subframe navigation requests are blocked in ExtensionNavigationThrottle
// which isn't added in this unit test. This is tested in an integration
// test in ExtensionResourceRequestPolicyTest.IframeNavigateToInaccessible.
RemoveExtension(extension, UnloadedExtensionReason::UNINSTALL);
}
}
// Tests getting a resource for a component extension works correctly, both when
// the extension is enabled and when it is disabled.
TEST_P(ExtensionProtocolsTest, ComponentResourceRequest) {
scoped_refptr<const Extension> extension =
CreateWebStoreExtension(GetParam());
AddExtension(extension, false, false);
// First test it with the extension enabled.
{
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("webstore_icon_16.png"),
network::mojom::RequestDestination::kVideo);
EXPECT_EQ(net::OK, get_result.result());
EXPECT_TRUE(get_result.HasContentLengthHeader());
EXPECT_EQ("image/png", get_result.GetResponseHeaderByName(
net::HttpRequestHeaders::kContentType));
// TODO(crbug.com/333078381): remove "Content-Security-Policy" header from
// images.
EXPECT_TRUE(get_result.HeaderIsPresent("Content-Security-Policy"));
}
// And then test it with the extension disabled.
RemoveExtension(extension, UnloadedExtensionReason::DISABLE);
{
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("webstore_icon_16.png"),
network::mojom::RequestDestination::kVideo);
EXPECT_EQ(net::OK, get_result.result());
EXPECT_TRUE(get_result.HasContentLengthHeader());
EXPECT_EQ("image/png", get_result.GetResponseHeaderByName(
net::HttpRequestHeaders::kContentType));
}
}
// Tests that a URL request for resource from an extension returns a few
// expected response headers.
TEST_P(ExtensionProtocolsTest, ResourceRequestResponseHeaders) {
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(GetParam());
AddExtension(extension, false, false);
{
auto get_result = RequestOrLoad(extension->ResolveExtensionURL("test.dat"),
network::mojom::RequestDestination::kVideo);
EXPECT_EQ(net::OK, get_result.result());
// Check that cache-related headers are set.
std::string etag = get_result.GetResponseHeaderByName("ETag");
EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));
EXPECT_EQ("no-cache", get_result.GetResponseHeaderByName("Cache-Control"));
// We set test.dat as web-accessible, so it should have CORS headers.
EXPECT_EQ(
"*", get_result.GetResponseHeaderByName("Access-Control-Allow-Origin"));
EXPECT_EQ("cross-origin", get_result.GetResponseHeaderByName(
"Cross-Origin-Resource-Policy"));
// Only background service worker script should be allowed to load as a
// service worker.
EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));
// COEP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));
// CSP header does not make sense in non-document responses
// TODO(crbug.com/333078381): remove "Content-Security-Policy" header from
// non-document responses and update this check.
EXPECT_TRUE(get_result.HeaderIsPresent("Content-Security-Policy"));
// COOP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
// Origin Trials header does not make sense in video resource responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Origin-Trial"));
}
}
// Tests that request for background script returns a few expected response
// headers.
TEST_P(ExtensionProtocolsTest, BackgroundScriptRequestResponseHeaders) {
const int manifest_version = GetParam();
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(manifest_version);
AddExtension(extension, false, false);
{
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("background.js"),
network::mojom::RequestDestination::kServiceWorker);
EXPECT_EQ(net::OK, get_result.result());
// Check that cache-related headers are set.
std::string etag = get_result.GetResponseHeaderByName("ETag");
EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));
EXPECT_EQ("no-cache", get_result.GetResponseHeaderByName("Cache-Control"));
// Background scripts are not web-accessible, so do not need CORS headers.
EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));
// Only background service worker script should be allowed to load as a
// service worker.
if (manifest_version == 3) {
EXPECT_EQ("/",
get_result.GetResponseHeaderByName("Service-Worker-Allowed"));
} else {
EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));
}
// COEP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));
// Even though CSP is currently not respected for service workers, it
// probably should be. We continue to send a CSP header for service worker
// scripts for when this changes.
// See also
// https://github.com/w3c/webappsec-csp/issues/336#issuecomment-1274730655
if (manifest_version == 3) {
EXPECT_EQ("script-src 'self';",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
} else {
EXPECT_EQ(
"script-src 'self' blob: filesystem:; object-src 'self' blob: "
"filesystem:;",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
}
// COOP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
}
}
// Tests that request for background service worker returns Origin-Trial
// response header.
TEST_P(ExtensionProtocolsMV3Test, BackgroundScriptRequestResponseHeaders) {
EXPECT_EQ(3, GetParam());
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(GetParam());
AddExtension(extension, false, false);
{
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("background.js"),
network::mojom::RequestDestination::kServiceWorker);
EXPECT_EQ(net::OK, get_result.result());
// In MV3-style service workers origin trail tokens are served via service
// worker Origin-Trial header.
EXPECT_EQ(kTrialTokensHeaderValue,
get_result.GetResponseHeaderByName("Origin-Trial"));
}
}
// TODO(crbug.com/333078381): Add a test checking that:
// - when background.page or background.service_worker is specified requesting
// generated background page fails
// - when no background is specified, requesting generated background page fails
TEST_P(ExtensionProtocolsTest, BackgroundPageRequestResponseHeaders) {
const int manifest_version = GetParam();
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(manifest_version);
AddExtension(extension, false, false);
{
auto get_result = RequestOrLoad(
extension->ResolveExtensionURL(kGeneratedBackgroundPageFilename),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::OK, get_result.result());
// Check that cache-related headers are omitted
// TODO(crbug.com/333078381): consider adding these headers to generated
// pages.
EXPECT_FALSE(get_result.HeaderIsPresent("ETag"));
EXPECT_FALSE(get_result.HeaderIsPresent("Cache-Control"));
// Background pages are not web-accessible, so do not need CORS headers.
EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));
// Background page does not need to be loaded as a service worker.
EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));
// Background page does not load cross-origin content so does not need COEP
// header.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));
if (manifest_version == 3) {
EXPECT_EQ("script-src 'self';",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
} else {
EXPECT_EQ(
"script-src 'self' blob: filesystem:; object-src 'self' blob: "
"filesystem:;",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
}
// COOP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
}
}
// Tests that resources from imported module extensions get appropriately
// loaded with proper headers or rejected
TEST_P(ExtensionProtocolsTest, ModuleRequestResponseHeaders) {
const int manifest_version = GetParam();
scoped_refptr<const Extension> module_extension =
CreateTestModuleResponseHeaderExtension(manifest_version);
scoped_refptr<const Extension> importer_extension =
CreateTestModuleImporterResponseHeaderExtension(manifest_version,
module_extension->id());
AddExtension(module_extension, false, false);
AddExtension(importer_extension, false, false);
// Not imported id will fail.
{
auto get_result =
RequestOrLoad(importer_extension->ResolveExtensionURL(
"_modules/modaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test.dat"),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
}
{
auto get_result =
RequestOrLoad(importer_extension->ResolveExtensionURL(
"_modules/modaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/test.dat"),
network::mojom::RequestDestination::kServiceWorker);
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
}
// Imported resources get loaded with proper headers (inherited from
// importer).
{
auto get_result =
RequestOrLoad(importer_extension->ResolveExtensionURL(
"_modules/" + module_extension->id() + "/test.dat"),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::OK, get_result.result());
// Check that cache-related headers are set.
std::string etag = get_result.GetResponseHeaderByName("ETag");
EXPECT_TRUE(base::StartsWith(etag, "\"", base::CompareCase::SENSITIVE));
EXPECT_TRUE(base::EndsWith(etag, "\"", base::CompareCase::SENSITIVE));
// Background pages are not web-accessible, so do not need CORS headers.
EXPECT_FALSE(get_result.HeaderIsPresent("Access-Control-Allow-Origin"));
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Resource-Policy"));
// Background page does not need to be loaded as a service worker.
EXPECT_FALSE(get_result.HeaderIsPresent("Service-Worker-Allowed"));
// Background page does not load cross-origin content so does not need COEP
// header.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Embedder-Policy"));
if (manifest_version == 3) {
EXPECT_EQ("script-src 'self';",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
} else {
EXPECT_EQ(
"script-src 'self' blob: filesystem:; object-src 'self' blob: "
"filesystem:;",
get_result.GetResponseHeaderByName("Content-Security-Policy"));
}
// COOP header does not make sense in non-document responses.
EXPECT_FALSE(get_result.HeaderIsPresent("Cross-Origin-Opener-Policy"));
}
}
// Tests that request for background service worker returns Origin-Trial
// response header.
TEST_P(ExtensionProtocolsMV3Test, ModuleRequestResponseHeaders) {
EXPECT_EQ(3, GetParam());
const int manifest_version = GetParam();
scoped_refptr<const Extension> module_extension =
CreateTestModuleResponseHeaderExtension(manifest_version);
scoped_refptr<const Extension> importer_extension =
CreateTestModuleImporterResponseHeaderExtension(manifest_version,
module_extension->id());
AddExtension(module_extension, false, false);
AddExtension(importer_extension, false, false);
// Imported resources get loaded with proper headers (inherited from
// importer).
{
auto get_result =
RequestOrLoad(importer_extension->ResolveExtensionURL(
"_modules/" + module_extension->id() + "/test.dat"),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::OK, get_result.result());
// Origin-Trial header should contain trials inherited from importer.
EXPECT_EQ(kTrialTokensHeaderValue,
get_result.GetResponseHeaderByName("Origin-Trial"));
}
}
TEST_P(ExtensionProtocolsTest, InvalidBackgroundScriptRequest) {
const int manifest_version = GetParam();
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(manifest_version);
AddExtension(extension, false, false);
// Requesting script from background key with invalid destination is
// forbidden.
std::vector<network::mojom::RequestDestination> destinations = {
// TODO(crbug.com/333078381): carefully consider which other
// request destinations should be allowed or blocked and update
// this test
network::mojom::RequestDestination::kJson,
network::mojom::RequestDestination::kStyle,
network::mojom::RequestDestination::kVideo,
};
for (network::mojom::RequestDestination destination : destinations) {
auto get_result = RequestOrLoad(
extension->ResolveExtensionURL("background.js"), destination);
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result()) << destination;
}
}
// Tests that a URL request for main frame or subframe from an extension
// succeeds, but subresources fail. See http://crbug.com/312269.
TEST_P(ExtensionProtocolsTest, AllowFrameRequests) {
scoped_refptr<const Extension> extension =
CreateTestExtension("foo", false, GetParam());
AddExtension(extension, false, false);
// All MAIN_FRAME requests should succeed. SUB_FRAME requests that are not
// explicitly listed in web_accessible_resources or same-origin to the parent
// should not succeed.
{
auto get_result =
RequestOrLoad(extension->ResolveExtensionURL("test.dat"),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::OK, get_result.result());
}
// Subframe navigation requests are blocked in ExtensionNavigationThrottle
// which isn't added in this unit test. This is tested in an integration test
// in ExtensionResourceRequestPolicyTest.IframeNavigateToInaccessible.
// And subresource types, such as media, should fail.
{
auto get_result = RequestOrLoad(extension->ResolveExtensionURL("test.dat"),
network::mojom::RequestDestination::kVideo);
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, get_result.result());
}
}
// Make sure requests for paths ending with a separator aren't allowed. See
// https://crbug.com/356878412.
TEST_P(ExtensionProtocolsTest, PathsWithTrailingSeparatorsAreNotAllowed) {
base::FilePath extension_dir = GetTestPath("simple_with_file");
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
&error);
ASSERT_NE(extension.get(), nullptr) << "error: " << error;
// Loading "/file.html" should succeed.
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, "file.html").result());
// Loading "/file.html/" should fail.
base::FilePath relative_path =
base::FilePath(FILE_PATH_LITERAL("file.html")).AsEndingWithSeparator();
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}
// Make sure requests for paths ending with a dot or a space aren't resolved to
// the corresponding file without the ending dot or space, as it normally would
// on Windows. See https://crbug.com/400119351.
TEST_P(ExtensionProtocolsTest, PathsWithTrailingDotSpaceAreNotAllowed) {
base::FilePath extension_dir = GetTestPath("simple_with_file");
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
&error);
ASSERT_NE(extension.get(), nullptr) << "error: " << error;
// Loading "/file.html" should succeed.
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, "file.html").result());
// Loading "/file.html." and "/file.html " should fail.
for (const std::string suffix : {".", "%20"}) {
// Add the suffix manually, as `ResolveExtensionURL` strips trailing spaces.
GURL url =
GURL(extension->ResolveExtensionURL("file.html").spec() + suffix);
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
RequestOrLoad(url, network::mojom::RequestDestination::kDocument)
.result());
}
}
// Make sure directories with an index.html file aren't serving the file, i.e.
// index.html doesn't get any special treatment.
TEST_P(ExtensionProtocolsTest, DirectoryWithIndexHtml) {
base::FilePath extension_dir = GetTestPath("simple_with_index_html");
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
&error);
ASSERT_NE(extension.get(), nullptr) << "error: " << error;
// Loading "/test_dir" should fail.
base::FilePath relative_path(FILE_PATH_LITERAL("test_dir"));
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
// Loading "/test_dir/" should fail.
relative_path = relative_path.AsEndingWithSeparator();
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
// Loading "/test_dir/index.html" explicitly should succeed.
relative_path = relative_path.AppendASCII("index.html");
EXPECT_EQ(net::OK,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}
TEST_P(ExtensionProtocolsTest, MetadataFolder) {
base::FilePath extension_dir = GetTestPath("metadata_folder");
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
&error);
ASSERT_NE(extension.get(), nullptr) << "error: " << error;
// Loading "/test.html" should succeed.
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, "test.html").result());
// Loading "/_metadata/verified_contents.json" should fail.
base::FilePath relative_path =
base::FilePath(kMetadataFolder).Append(kVerifiedContentsFilename);
EXPECT_TRUE(base::PathExists(extension_dir.Append(relative_path)));
EXPECT_NE(net::OK,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
// Loading "/_metadata/a.txt" should also fail.
relative_path = base::FilePath(kMetadataFolder).AppendASCII("a.txt");
EXPECT_TRUE(base::PathExists(extension_dir.Append(relative_path)));
EXPECT_NE(net::OK,
DoRequestOrLoad(extension, relative_path.AsUTF8Unsafe()).result());
}
// Tests that unreadable files and deleted files correctly go through
// ContentVerifyJob.
TEST_P(ExtensionProtocolsTest, VerificationSeenForFileAccessErrors) {
// Unzip extension containing verification hashes to a temporary directory.
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath unzipped_path = temp_dir.GetPath();
scoped_refptr<Extension> extension =
content_verifier_test_utils::UnzipToDirAndLoadExtension(
GetContentVerifierTestPath().AppendASCII("source.zip"),
unzipped_path);
ASSERT_TRUE(extension.get());
ExtensionId extension_id = extension->id();
const std::string kJs("1024.js");
base::FilePath kRelativePath(FILE_PATH_LITERAL("1024.js"));
// Valid and readable 1024.js.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kJs).result());
EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
}
// chmod -r 1024.js.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
base::FilePath file_path = unzipped_path.AppendASCII(kJs);
ASSERT_TRUE(base::MakeFileUnreadable(file_path));
EXPECT_EQ(net::ERR_ACCESS_DENIED, DoRequestOrLoad(extension, kJs).result());
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
// NOTE: In production, hash mismatch would have disabled |extension|, but
// since UnzipToDirAndLoadExtension() doesn't add the extension to
// ExtensionRegistry, ChromeContentVerifierDelegate won't disable it.
// TODO(lazyboy): We may want to update this to more closely reflect the
// real flow.
}
// Delete 1024.js.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
base::FilePath file_path = unzipped_path.AppendASCII(kJs);
ASSERT_TRUE(base::DieFileDie(file_path, false));
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
DoRequestOrLoad(extension, kJs).result());
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
}
}
// Tests that zero byte files correctly go through ContentVerifyJob.
TEST_P(ExtensionProtocolsTest, VerificationSeenForZeroByteFile) {
const std::string kEmptyJs("empty.js");
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath unzipped_path = temp_dir.GetPath();
scoped_refptr<Extension> extension =
content_verifier_test_utils::UnzipToDirAndLoadExtension(
GetContentVerifierTestPath().AppendASCII("source.zip"),
unzipped_path);
ASSERT_TRUE(extension.get());
base::FilePath kRelativePath(FILE_PATH_LITERAL("empty.js"));
ExtensionId extension_id = extension->id();
// Sanity check empty.js.
base::FilePath file_path = unzipped_path.AppendASCII(kEmptyJs);
std::optional<int64_t> foo_file_size = base::GetFileSize(file_path);
ASSERT_TRUE(foo_file_size.has_value());
ASSERT_EQ(0, foo_file_size.value());
// Request empty.js.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kEmptyJs).result());
EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
}
// chmod -r empty.js.
// Unreadable empty file results in hash mismatch.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
ASSERT_TRUE(base::MakeFileUnreadable(file_path));
EXPECT_EQ(net::ERR_ACCESS_DENIED,
DoRequestOrLoad(extension, kEmptyJs).result());
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
}
// rm empty.js.
// Deleted empty file results in hash mismatch.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
ASSERT_TRUE(base::DieFileDie(file_path, false));
EXPECT_EQ(net::ERR_FILE_NOT_FOUND,
DoRequestOrLoad(extension, kEmptyJs).result());
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
}
}
TEST_P(ExtensionProtocolsTest, VerifyScriptListedAsIcon) {
const std::string kBackgroundJs("background.js");
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath unzipped_path = temp_dir.GetPath();
base::FilePath path;
EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &path));
scoped_refptr<Extension> extension =
content_verifier_test_utils::UnzipToDirAndLoadExtension(
path.AppendASCII("content_hash_fetcher")
.AppendASCII("manifest_mislabeled_script")
.AppendASCII("source.zip"),
unzipped_path);
ASSERT_TRUE(extension.get());
base::FilePath kRelativePath(FILE_PATH_LITERAL("background.js"));
ExtensionId extension_id = extension->id();
// Request background.js.
{
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kBackgroundJs).result());
EXPECT_EQ(ContentVerifyJob::NONE, observer.WaitForJobFinished());
}
// Modify background.js and request it.
{
base::FilePath file_path = unzipped_path.AppendASCII("background.js");
const std::string content = "new content";
EXPECT_TRUE(base::WriteFile(file_path, content));
TestContentVerifySingleJobObserver observer(extension_id, kRelativePath);
EXPECT_EQ(net::OK, DoRequestOrLoad(extension, kBackgroundJs).result());
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH, observer.WaitForJobFinished());
}
}
// Tests that mime types are properly set for returned extension resources.
TEST_P(ExtensionProtocolsTest, MimeTypesForKnownFiles) {
TestExtensionDir test_dir;
const int manifest_version = GetParam();
constexpr char kManifestV2[] = R"(
{
"name": "Test Ext",
"manifest_version": 2,
"version": "1",
"web_accessible_resources": ["*"]
})";
constexpr char kManifestV3[] = R"(
{
"name": "Test Ext",
"manifest_version": 3,
"version": "1",
"web_accessible_resources": [{
"resources": [ "*" ],
"matches": [ "*://*/*" ]
}]
})";
const char* kManifest = manifest_version == 3 ? kManifestV3 : kManifestV2;
test_dir.WriteManifest(kManifest);
base::Value::Dict manifest = base::test::ParseJsonDict(kManifest);
ASSERT_FALSE(manifest.empty());
test_dir.WriteFile(FILE_PATH_LITERAL("json_file.json"), "{}");
test_dir.WriteFile(FILE_PATH_LITERAL("js_file.js"), "function() {}");
base::FilePath unpacked_path = test_dir.UnpackedPath();
ASSERT_TRUE(base::PathExists(unpacked_path.AppendASCII("json_file.json")));
std::string error;
scoped_refptr<const Extension> extension =
ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetPath(unpacked_path)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
ASSERT_TRUE(extension);
AddExtension(extension.get(), false, false);
struct {
const char* file_name;
const char* expected_mime_type;
} test_cases[] = {
{"json_file.json", "application/json"},
{"js_file.js", "text/javascript"},
{"mem_file.mem", ""},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.file_name);
EXPECT_EQ(
test_case.expected_mime_type,
RequestOrLoad(extension->ResolveExtensionURL(test_case.file_name),
network::mojom::RequestDestination::kEmpty)
.GetResponseHeaderByName(net::HttpRequestHeaders::kContentType));
}
}
// Tests that requests for extension resources (including the generated
// background page) are not aborted on system suspend.
TEST_P(ExtensionProtocolsTest, ExtensionRequestsNotAborted) {
base::FilePath extension_dir =
GetTestPath("common").AppendASCII("background_script");
std::string error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, mojom::ManifestLocation::kInternal, Extension::NO_FLAGS,
&error);
ASSERT_TRUE(extension.get()) << error;
EnableSimulationOfSystemSuspendForRequests();
// Request the generated background page. Ensure the request completes
// successfully.
EXPECT_EQ(net::OK,
DoRequestOrLoad(extension.get(), kGeneratedBackgroundPageFilename)
.result());
// Request the background.js file. Ensure the request completes successfully.
EXPECT_EQ(net::OK,
DoRequestOrLoad(extension.get(), "background.js").result());
}
// Tests that mime type sniffing is not performed for extension resources.
TEST_P(ExtensionProtocolsTest, MimeTypeSniffingNotPerformed) {
scoped_refptr<const Extension> extension =
CreateTestResponseHeaderExtension(GetParam());
AddExtension(extension, false, false);
auto get_result = RequestOrLoad(
extension->ResolveExtensionURL("mime_type_sniffer_test.gif1"),
network::mojom::RequestDestination::kDocument);
EXPECT_EQ(net::OK, get_result.result());
// With mime sniffing, the content type would be image/gif.
EXPECT_EQ("application/octet-stream",
get_result.GetResponseHeaderByName("Content-Type"));
}
} // namespace extensions