blob: 8196752f69cbdc64abecb7ec99c1fd53d5275947 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/sandboxed_unpacker.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "components/crx_file/id_util.h"
#include "components/services/unzip/content/unzip_service.h"
#include "components/services/unzip/in_process_unzipper.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/browser/install/crx_install_error.h"
#include "extensions/browser/install/sandboxed_unpacker_failure_reason.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/switches.h"
#include "extensions/common/value_builder.h"
#include "extensions/common/verifier_formats.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "extensions/test/test_extensions_client.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/zlib/google/zip.h"
#include "ui/base/l10n/l10n_util.h"
namespace extensions {
namespace {
// Inserts an illegal path into the browser images returned by
// TestExtensionsClient for any extension.
class IllegalImagePathInserter
: public TestExtensionsClient::BrowserImagePathsFilter {
public:
IllegalImagePathInserter(TestExtensionsClient* client) : client_(client) {
client_->AddBrowserImagePathsFilter(this);
}
virtual ~IllegalImagePathInserter() {
client_->RemoveBrowserImagePathsFilter(this);
}
void Filter(const Extension* extension,
std::set<base::FilePath>* paths) override {
base::FilePath illegal_path =
base::FilePath(base::FilePath::kParentDirectory)
.AppendASCII(kTempExtensionName)
.AppendASCII("product_logo_128.png");
paths->insert(illegal_path);
}
private:
TestExtensionsClient* client_;
};
} // namespace
class MockSandboxedUnpackerClient : public SandboxedUnpackerClient {
public:
void WaitForUnpack() {
scoped_refptr<content::MessageLoopRunner> runner =
new content::MessageLoopRunner;
quit_closure_ = runner->QuitClosure();
runner->Run();
}
base::FilePath temp_dir() const { return temp_dir_; }
base::string16 unpack_error_message() const {
if (error_)
return error_->message();
return base::string16();
}
CrxInstallErrorType unpack_error_type() const {
if (error_)
return error_->type();
return CrxInstallErrorType::NONE;
}
int unpack_error_detail() const {
if (error_) {
return error_->type() == CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE
? static_cast<int>(error_->sandbox_failure_detail())
: static_cast<int>(error_->detail());
}
return 0;
}
void set_deleted_tracker(bool* deleted_tracker) {
deleted_tracker_ = deleted_tracker;
}
void set_should_compute_hashes(bool should_compute_hashes) {
should_compute_hashes_ = should_compute_hashes;
}
private:
~MockSandboxedUnpackerClient() override {
if (deleted_tracker_)
*deleted_tracker_ = true;
}
void ShouldComputeHashesForOffWebstoreExtension(
scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback) override {
std::move(callback).Run(should_compute_hashes_);
}
void OnUnpackSuccess(const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
const Extension* extension,
const SkBitmap& install_icon,
declarative_net_request::RulesetInstallPrefs
ruleset_install_prefs) override {
temp_dir_ = temp_dir;
std::move(quit_closure_).Run();
}
void OnUnpackFailure(const CrxInstallError& error) override {
error_ = error;
std::move(quit_closure_).Run();
}
base::Optional<CrxInstallError> error_;
base::OnceClosure quit_closure_;
base::FilePath temp_dir_;
bool* deleted_tracker_ = nullptr;
bool should_compute_hashes_ = false;
};
class SandboxedUnpackerTest : public ExtensionsTest {
public:
SandboxedUnpackerTest()
: ExtensionsTest(content::BrowserTaskEnvironment::IO_MAINLOOP) {}
void SetUp() override {
ExtensionsTest::SetUp();
ASSERT_TRUE(extensions_dir_.CreateUniqueTempDir());
in_process_utility_thread_helper_.reset(
new content::InProcessUtilityThreadHelper);
// It will delete itself.
client_ = new MockSandboxedUnpackerClient;
InitSandboxedUnpacker();
// By default, we host an in-process UnzipperImpl to support any service
// clients. Tests may explicitly override the launch callback to prevent
// this.
unzip::SetUnzipperLaunchOverrideForTesting(
base::BindRepeating(&unzip::LaunchInProcessUnzipper));
}
void InitSandboxedUnpacker() {
sandboxed_unpacker_ = new SandboxedUnpacker(
Manifest::INTERNAL, Extension::NO_FLAGS, extensions_dir_.GetPath(),
base::ThreadTaskRunnerHandle::Get(), client_);
}
void TearDown() override {
unzip::SetUnzipperLaunchOverrideForTesting(base::NullCallback());
// Need to destruct SandboxedUnpacker before the message loop since
// it posts a task to it.
sandboxed_unpacker_ = nullptr;
base::RunLoop().RunUntilIdle();
ExtensionsTest::TearDown();
in_process_utility_thread_helper_.reset();
}
base::FilePath GetCrxFullPath(const std::string& crx_name) {
base::FilePath full_path;
EXPECT_TRUE(base::PathService::Get(extensions::DIR_TEST_DATA, &full_path));
full_path = full_path.AppendASCII("unpacker").AppendASCII(crx_name);
EXPECT_TRUE(base::PathExists(full_path)) << full_path.value();
return full_path;
}
void SetupUnpacker(const std::string& crx_name,
const std::string& package_hash) {
base::FilePath crx_path = GetCrxFullPath(crx_name);
extensions::CRXFileInfo crx_info(crx_path, GetTestVerifierFormat());
crx_info.expected_hash = package_hash;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&SandboxedUnpacker::StartWithCrx,
sandboxed_unpacker_, crx_info));
client_->WaitForUnpack();
}
void SetupUnpackerWithDirectory(const std::string& crx_name) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath crx_path = GetCrxFullPath(crx_name);
ASSERT_TRUE(zip::Unzip(crx_path, temp_dir.GetPath()));
std::string fake_id = crx_file::id_util::GenerateId(crx_name);
std::string fake_public_key;
base::Base64Encode(std::string(2048, 'k'), &fake_public_key);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&SandboxedUnpacker::StartWithDirectory,
sandboxed_unpacker_, fake_id, fake_public_key,
temp_dir.Take()));
client_->WaitForUnpack();
}
bool InstallSucceeded() const { return !client_->temp_dir().empty(); }
base::FilePath GetInstallPath() const {
return client_->temp_dir().AppendASCII(kTempExtensionName);
}
base::string16 GetInstallErrorMessage() const {
return client_->unpack_error_message();
}
CrxInstallErrorType GetInstallErrorType() const {
return client_->unpack_error_type();
}
int GetInstallErrorDetail() const { return client_->unpack_error_detail(); }
void ExpectInstallErrorContains(const std::string& error) {
std::string full_error = base::UTF16ToUTF8(client_->unpack_error_message());
EXPECT_TRUE(full_error.find(error) != std::string::npos)
<< "Error message " << full_error << " does not contain " << error;
}
// Unpacks the package |package_name| and checks that |sandboxed_unpacker_|
// gets deleted.
void TestSandboxedUnpackerDeleted(const std::string& package_name,
bool expect_success) {
bool client_deleted = false;
client_->set_deleted_tracker(&client_deleted);
SetupUnpacker(package_name, "");
EXPECT_EQ(GetInstallErrorMessage().empty(), expect_success);
// Remove our reference to |sandboxed_unpacker_|, it should get deleted
// since/ it's the last reference.
sandboxed_unpacker_ = nullptr;
// The SandboxedUnpacker should have been deleted and deleted the client.
EXPECT_TRUE(client_deleted);
}
void SetPublicKey(const std::string& key) {
sandboxed_unpacker_->public_key_ = key;
}
void SetExtensionRoot(const base::FilePath& path) {
sandboxed_unpacker_->extension_root_ = path;
}
base::Optional<base::Value> RewriteManifestFile(const base::Value& manifest) {
return sandboxed_unpacker_->RewriteManifestFile(manifest);
}
data_decoder::test::InProcessDataDecoder& in_process_data_decoder() {
return in_process_data_decoder_;
}
protected:
base::ScopedTempDir extensions_dir_;
MockSandboxedUnpackerClient* client_;
scoped_refptr<SandboxedUnpacker> sandboxed_unpacker_;
std::unique_ptr<content::InProcessUtilityThreadHelper>
in_process_utility_thread_helper_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};
TEST_F(SandboxedUnpackerTest, EmptyDefaultLocale) {
SetupUnpacker("empty_default_locale.crx", "");
ExpectInstallErrorContains(manifest_errors::kInvalidDefaultLocale);
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, HasDefaultLocaleMissingLocalesFolder) {
SetupUnpacker("has_default_missing_locales.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesTreeMissing);
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, InvalidDefaultLocale) {
SetupUnpacker("invalid_default_locale.crx", "");
ExpectInstallErrorContains(manifest_errors::kInvalidDefaultLocale);
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, MissingDefaultData) {
SetupUnpacker("missing_default_data.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesNoDefaultMessages);
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, MissingDefaultLocaleHasLocalesFolder) {
SetupUnpacker("missing_default_has_locales.crx", "");
ExpectInstallErrorContains(l10n_util::GetStringUTF8(
IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED));
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, MissingMessagesFile) {
SetupUnpacker("missing_messages_file.crx", "");
EXPECT_TRUE(base::MatchPattern(
GetInstallErrorMessage(),
base::ASCIIToUTF16("*") +
base::ASCIIToUTF16(manifest_errors::kLocalesMessagesFileMissing) +
base::ASCIIToUTF16("*_locales?en_US?messages.json'.")))
<< GetInstallErrorMessage();
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, NoLocaleData) {
SetupUnpacker("no_locale_data.crx", "");
ExpectInstallErrorContains(manifest_errors::kLocalesNoDefaultMessages);
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, ImageDecodingError) {
const char kExpected[] = "Could not decode image: ";
SetupUnpacker("bad_image.crx", "");
EXPECT_TRUE(base::StartsWith(GetInstallErrorMessage(),
base::ASCIIToUTF16(kExpected),
base::CompareCase::INSENSITIVE_ASCII))
<< "Expected prefix: \"" << kExpected << "\", actual error: \""
<< GetInstallErrorMessage() << "\"";
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, BadPathError) {
IllegalImagePathInserter inserter(
static_cast<TestExtensionsClient*>(ExtensionsClient::Get()));
SetupUnpacker("good_package.crx", "");
// Install should have failed with an error.
EXPECT_FALSE(InstallSucceeded());
EXPECT_FALSE(GetInstallErrorMessage().empty());
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(static_cast<int>(
SandboxedUnpackerFailureReason::INVALID_PATH_FOR_BROWSER_IMAGE),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, NoCatalogsSuccess) {
SetupUnpacker("no_l10n.crx", "");
// Check that there is no _locales folder.
base::FilePath install_path = GetInstallPath().Append(kLocaleFolder);
EXPECT_FALSE(base::PathExists(install_path));
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, FromDirNoCatalogsSuccess) {
SetupUnpackerWithDirectory("no_l10n.crx");
// Check that there is no _locales folder.
base::FilePath install_path = GetInstallPath().Append(kLocaleFolder);
EXPECT_FALSE(base::PathExists(install_path));
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, WithCatalogsSuccess) {
SetupUnpacker("good_l10n.crx", "");
// Check that there is _locales folder.
base::FilePath install_path = GetInstallPath().Append(kLocaleFolder);
EXPECT_TRUE(base::PathExists(install_path));
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, FromDirWithCatalogsSuccess) {
SetupUnpackerWithDirectory("good_l10n.crx");
// Check that there is _locales folder.
base::FilePath install_path = GetInstallPath().Append(kLocaleFolder);
EXPECT_TRUE(base::PathExists(install_path));
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, FailHashCheck) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
extensions::switches::kEnableCrxHashCheck);
SetupUnpacker("good_l10n.crx", std::string(64, '0'));
// Check that there is an error message.
EXPECT_FALSE(GetInstallErrorMessage().empty());
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(static_cast<int>(
SandboxedUnpackerFailureReason::CRX_HASH_VERIFICATION_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, TestRewriteManifestInjections) {
constexpr char kTestKey[] = "test_key";
constexpr char kTestVersion[] = "1.2.3";
constexpr char kVersionStr[] = "version";
SetPublicKey(kTestKey);
SetExtensionRoot(extensions_dir_.GetPath());
std::string fingerprint = "1.0123456789abcdef";
base::WriteFile(extensions_dir_.GetPath().Append(
FILE_PATH_LITERAL("manifest.fingerprint")),
fingerprint.c_str(),
base::checked_cast<int>(fingerprint.size()));
base::Optional<base::Value> manifest(RewriteManifestFile(
*DictionaryBuilder().Set(kVersionStr, kTestVersion).Build()));
auto* key = manifest->FindStringKey("key");
auto* version = manifest->FindStringKey(kVersionStr);
auto* differential_fingerprint =
manifest->FindStringKey("differential_fingerprint");
ASSERT_NE(nullptr, key);
ASSERT_NE(nullptr, version);
ASSERT_NE(nullptr, differential_fingerprint);
EXPECT_EQ(kTestKey, *key);
EXPECT_EQ(kTestVersion, *version);
EXPECT_EQ(fingerprint, *differential_fingerprint);
}
TEST_F(SandboxedUnpackerTest, InvalidMessagesFile) {
SetupUnpackerWithDirectory("invalid_messages_file.crx");
// Check that there is no _locales folder.
base::FilePath install_path = GetInstallPath().Append(kLocaleFolder);
EXPECT_FALSE(base::PathExists(install_path));
EXPECT_TRUE(base::MatchPattern(
GetInstallErrorMessage(),
base::ASCIIToUTF16(
"*_locales?en_US?messages.json': Line: 4, column: 1,*")))
<< GetInstallErrorMessage();
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(static_cast<int>(
SandboxedUnpackerFailureReason::COULD_NOT_LOCALIZE_EXTENSION),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, PassHashCheck) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
extensions::switches::kEnableCrxHashCheck);
SetupUnpacker(
"good_l10n.crx",
"614AE3D608F4C2185E9173293AB3F93EE7C7C79C9A2C3CF71F633386A3296A6C");
// Check that there is no error message.
EXPECT_THAT(GetInstallErrorMessage(), testing::IsEmpty());
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, SkipHashCheck) {
SetupUnpacker("good_l10n.crx", "badhash");
// Check that there is no error message.
EXPECT_THAT(GetInstallErrorMessage(), testing::IsEmpty());
EXPECT_EQ(CrxInstallErrorType::NONE, GetInstallErrorType());
}
// The following tests simulate the utility services failling.
TEST_F(SandboxedUnpackerTest, UnzipperServiceFails) {
// We override the Unzipper's launching behavior to drop the interface
// receiver, effectively simulating a crashy service process.
unzip::SetUnzipperLaunchOverrideForTesting(base::BindRepeating([]() -> auto {
mojo::PendingRemote<unzip::mojom::Unzipper> remote;
ignore_result(remote.InitWithNewPipeAndPassReceiver());
return remote;
}));
InitSandboxedUnpacker();
SetupUnpacker("good_package.crx", "");
EXPECT_FALSE(InstallSucceeded());
EXPECT_FALSE(GetInstallErrorMessage().empty());
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(static_cast<int>(SandboxedUnpackerFailureReason::UNZIP_FAILED),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, JsonParserFails) {
in_process_data_decoder().service().SimulateJsonParserCrashForTesting(true);
InitSandboxedUnpacker();
SetupUnpacker("good_package.crx", "");
EXPECT_FALSE(InstallSucceeded());
EXPECT_FALSE(GetInstallErrorMessage().empty());
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
}
TEST_F(SandboxedUnpackerTest, ImageDecoderFails) {
in_process_data_decoder().service().SimulateImageDecoderCrashForTesting(true);
InitSandboxedUnpacker();
SetupUnpacker("good_package.crx", "");
EXPECT_FALSE(InstallSucceeded());
EXPECT_FALSE(GetInstallErrorMessage().empty());
ASSERT_EQ(CrxInstallErrorType::SANDBOXED_UNPACKER_FAILURE,
GetInstallErrorType());
EXPECT_EQ(
static_cast<int>(SandboxedUnpackerFailureReason::
UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL),
GetInstallErrorDetail());
}
TEST_F(SandboxedUnpackerTest, NoComputeHashes) {
client_->set_should_compute_hashes(false);
SetupUnpacker("good_package.crx", "");
EXPECT_TRUE(InstallSucceeded());
EXPECT_TRUE(GetInstallErrorMessage().empty());
EXPECT_FALSE(
base::PathExists(file_util::GetComputedHashesPath(GetInstallPath())));
}
TEST_F(SandboxedUnpackerTest, ComputeHashes) {
client_->set_should_compute_hashes(true);
SetupUnpacker("good_package.crx", "");
EXPECT_TRUE(InstallSucceeded());
EXPECT_TRUE(GetInstallErrorMessage().empty());
EXPECT_TRUE(
base::PathExists(file_util::GetComputedHashesPath(GetInstallPath())));
}
// SandboxedUnpacker is ref counted and is reference by callbacks and
// InterfacePtrs. This tests that it gets deleted as expected (so that no extra
// refs are left).
TEST_F(SandboxedUnpackerTest, DeletedOnSuccess) {
TestSandboxedUnpackerDeleted("good_l10n.crx", /*expect_success=*/true);
}
TEST_F(SandboxedUnpackerTest, DeletedOnFailure) {
TestSandboxedUnpackerDeleted("bad_image.crx", /*expect_success=*/false);
}
} // namespace extensions