blob: 8b10d8f0ad045de37759b9df1d633912f5a180c5 [file] [log] [blame]
// Copyright 2017 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 "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/task/post_task.h"
#include "base/version.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "extensions/browser/content_verifier.h"
#include "extensions/browser/content_verifier/test_utils.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/browser/info_map.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_paths.h"
#include "extensions/common/file_util.h"
#include "net/url_request/url_request_job_factory_impl.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/zip.h"
namespace extensions {
namespace {
// Specifies how test ContentVerifyJob's asynchronous steps to read hash and
// read contents are ordered.
// Note that:
// OnHashesReady: is called when hash reading is complete.
// BytesRead + DoneReading: are called when content reading is complete.
enum ContentVerifyJobAsyncRunMode {
// None - Let hash reading and content reading continue as is asynchronously.
kNone,
// Hashes become available after the contents become available.
kContentReadBeforeHashesReady,
// The contents become available before the hashes are ready.
kHashesReadyBeforeContentRead,
};
} // namespace
class ContentVerifyJobUnittest : public ExtensionsTest {
public:
ContentVerifyJobUnittest() {}
~ContentVerifyJobUnittest() override {}
// Helper to get files from our subdirectory in the general extensions test
// data dir.
base::FilePath GetTestPath(const std::string& relative_path) {
base::FilePath base_path;
EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &base_path));
return base_path.AppendASCII("content_hash_fetcher")
.AppendASCII(relative_path);
}
void SetUp() override {
ExtensionsTest::SetUp();
extension_info_map_ = new InfoMap();
old_factory_ = test_url_request_context_.job_factory();
content_verifier_ = new ContentVerifier(
&testing_context_, std::make_unique<MockContentVerifierDelegate>());
extension_info_map_->SetContentVerifier(content_verifier_.get());
}
void TearDown() override {
test_url_request_context_.set_job_factory(old_factory_);
content_verifier_->Shutdown();
ExtensionsTest::TearDown();
}
ContentVerifier* content_verifier() { return content_verifier_.get(); }
protected:
ContentVerifyJob::FailureReason RunContentVerifyJob(
const Extension& extension,
const base::FilePath& resource_path,
std::string& resource_contents,
ContentVerifyJobAsyncRunMode run_mode) {
TestContentVerifySingleJobObserver observer(extension.id(), resource_path);
scoped_refptr<ContentVerifyJob> verify_job = new ContentVerifyJob(
extension.id(), extension.version(), extension.path(), resource_path,
base::DoNothing());
auto run_content_read_step = [](ContentVerifyJob* verify_job,
std::string* resource_contents) {
// Simulate serving |resource_contents| from |resource_path|.
verify_job->BytesRead(resource_contents->size(),
base::data(*resource_contents));
verify_job->DoneReading();
};
switch (run_mode) {
case kNone:
StartJob(verify_job); // Read hashes asynchronously.
run_content_read_step(verify_job.get(), &resource_contents);
break;
case kContentReadBeforeHashesReady:
run_content_read_step(verify_job.get(), &resource_contents);
StartJob(verify_job); // Read hashes asynchronously.
break;
case kHashesReadyBeforeContentRead:
StartJob(verify_job);
// Wait for hashes to become ready.
observer.WaitForOnHashesReady();
run_content_read_step(verify_job.get(), &resource_contents);
break;
};
return observer.WaitForJobFinished();
}
ContentVerifyJob::FailureReason RunContentVerifyJob(
const Extension& extension,
const base::FilePath& resource_path,
std::string& resource_contents) {
return RunContentVerifyJob(extension, resource_path, resource_contents,
kNone);
}
// Returns an extension after extracting and loading it from a .zip file.
// The extension is expected to have verified_contents.json in it.
scoped_refptr<Extension> LoadTestExtensionFromZipPathToTempDir(
base::ScopedTempDir* temp_dir,
const std::string& zip_directory_name,
const std::string& zip_filename) {
if (!temp_dir->CreateUniqueTempDir()) {
ADD_FAILURE() << "Failed to create temp dir.";
return nullptr;
}
base::FilePath unzipped_path = temp_dir->GetPath();
base::FilePath test_dir_base = GetTestPath(zip_directory_name);
scoped_refptr<Extension> extension =
content_verifier_test_utils::UnzipToDirAndLoadExtension(
test_dir_base.AppendASCII(zip_filename), unzipped_path);
// Make sure there is a verified_contents.json file there as this test
// cannot fetch it.
if (extension && !base::PathExists(file_util::GetVerifiedContentsPath(
extension->path()))) {
ADD_FAILURE() << "verified_contents.json not found.";
return nullptr;
}
return extension;
}
private:
void StartJob(scoped_refptr<ContentVerifyJob> job) {
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&ContentVerifyJob::Start, job,
base::Unretained(content_verifier_.get())));
}
scoped_refptr<InfoMap> extension_info_map_;
net::URLRequestJobFactoryImpl job_factory_;
const net::URLRequestJobFactory* old_factory_;
net::TestURLRequestContext test_url_request_context_;
scoped_refptr<ContentVerifier> content_verifier_;
content::TestBrowserContext testing_context_;
DISALLOW_COPY_AND_ASSIGN(ContentVerifyJobUnittest);
};
// Tests that deleted legitimate files trigger content verification failure.
// Also tests that non-existent file request does not trigger content
// verification failure.
TEST_F(ContentVerifyJobUnittest, DeletedAndMissingFiles) {
base::ScopedTempDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.GetPath();
const base::FilePath::CharType kExistentResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kExistentResource);
{
// Make sure background.js passes verification correctly.
std::string contents;
base::ReadFileToString(
unzipped_path.Append(base::FilePath(kExistentResource)), &contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), existent_resource_path,
contents));
}
{
// Once background.js is deleted, verification will result in HASH_MISMATCH.
// Delete the existent file first.
EXPECT_TRUE(base::DeleteFile(
unzipped_path.Append(base::FilePath(kExistentResource)), false));
// Deleted file will serve empty contents.
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
empty_contents));
}
{
// Now ask for a non-existent resource non-existent.js. Verification should
// skip this file as it is not listed in our verified_contents.json file.
const base::FilePath::CharType kNonExistentResource[] =
FILE_PATH_LITERAL("non-existent.js");
base::FilePath non_existent_resource_path(kNonExistentResource);
// Non-existent file will serve empty contents.
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), non_existent_resource_path,
empty_contents));
}
{
// Now create a resource foo.js which exists on disk but is not in the
// extension's verified_contents.json. Verification should result in
// NO_HASHES_FOR_FILE since the extension is trying to load a file the
// extension should not have.
const base::FilePath::CharType kUnexpectedResource[] =
FILE_PATH_LITERAL("foo.js");
base::FilePath unexpected_resource_path(kUnexpectedResource);
base::FilePath full_path =
unzipped_path.Append(base::FilePath(unexpected_resource_path));
const std::string kContent("42");
EXPECT_EQ(static_cast<int>(kContent.size()),
base::WriteFile(full_path, kContent.data(), kContent.size()));
std::string contents;
base::ReadFileToString(full_path, &contents);
EXPECT_EQ(ContentVerifyJob::NO_HASHES_FOR_FILE,
RunContentVerifyJob(*extension.get(), unexpected_resource_path,
contents));
}
{
// Ask for the root path of the extension (i.e., chrome-extension://<id>/).
// Verification should skip this request as if the resource were
// non-existent. See https://crbug.com/791929.
base::FilePath empty_path_resource_path(FILE_PATH_LITERAL(""));
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), empty_path_resource_path,
empty_contents));
}
{
// Ask for the path of one of the extension's folders which exists on disk.
// Verification of the folder should skip the request as if the folder
// was non-existent. See https://crbug.com/791929.
const base::FilePath::CharType kUnexpectedFolder[] =
FILE_PATH_LITERAL("bar/");
base::FilePath unexpected_folder_path(kUnexpectedFolder);
base::CreateDirectory(unzipped_path.Append(unexpected_folder_path));
std::string empty_contents;
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), unexpected_folder_path,
empty_contents));
}
}
// Tests that extension resources that are originally 0 byte behave correctly
// with content verification.
TEST_F(ContentVerifyJobUnittest, LegitimateZeroByteFile) {
base::ScopedTempDir temp_dir;
// |extension| has a 0 byte background.js file in it.
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "zero_byte_file", "source.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.GetPath();
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath resource_path(kResource);
{
// Make sure 0 byte background.js passes content verification.
std::string contents;
base::ReadFileToString(unzipped_path.Append(base::FilePath(kResource)),
&contents);
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), resource_path, contents));
}
{
// Make sure non-empty background.js fails content verification.
std::string modified_contents = "console.log('non empty');";
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), resource_path,
modified_contents));
}
}
// Tests that extension resources of different interesting sizes work properly.
// Regression test for https://crbug.com/720597, where content verification
// always failed for sizes multiple of content hash's block size (4096 bytes).
TEST_F(ContentVerifyJobUnittest, DifferentSizedFiles) {
base::ScopedTempDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "different_sized_files", "source.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.GetPath();
const struct {
const char* name;
size_t byte_size;
} kFilesToTest[] = {
{"1024.js", 1024}, {"4096.js", 4096}, {"8192.js", 8192},
{"8191.js", 8191}, {"8193.js", 8193},
};
for (const auto& file_to_test : kFilesToTest) {
base::FilePath resource_path =
base::FilePath::FromUTF8Unsafe(file_to_test.name);
std::string contents;
base::ReadFileToString(unzipped_path.AppendASCII(file_to_test.name),
&contents);
EXPECT_EQ(file_to_test.byte_size, contents.size());
EXPECT_EQ(ContentVerifyJob::NONE,
RunContentVerifyJob(*extension.get(), resource_path, contents));
}
}
class ContentMismatchUnittest
: public ContentVerifyJobUnittest,
public testing::WithParamInterface<ContentVerifyJobAsyncRunMode> {
public:
ContentMismatchUnittest() {}
protected:
// Runs test to verify that a modified extension resource (background.js)
// causes ContentVerifyJob to fail with HASH_MISMATCH. The string
// |content_to_append_for_mismatch| is appended to the resource for
// modification. The asynchronous nature of ContentVerifyJob can be controlled
// by |run_mode|.
void RunContentMismatchTest(const std::string& content_to_append_for_mismatch,
ContentVerifyJobAsyncRunMode run_mode) {
base::ScopedTempDir temp_dir;
scoped_refptr<Extension> extension = LoadTestExtensionFromZipPathToTempDir(
&temp_dir, "with_verified_contents", "source_all.zip");
ASSERT_TRUE(extension.get());
base::FilePath unzipped_path = temp_dir.GetPath();
const base::FilePath::CharType kResource[] =
FILE_PATH_LITERAL("background.js");
base::FilePath existent_resource_path(kResource);
{
// Make sure modified background.js fails content verification.
std::string modified_contents;
base::ReadFileToString(unzipped_path.Append(base::FilePath(kResource)),
&modified_contents);
modified_contents.append(content_to_append_for_mismatch);
EXPECT_EQ(ContentVerifyJob::HASH_MISMATCH,
RunContentVerifyJob(*extension.get(), existent_resource_path,
modified_contents, run_mode));
}
}
private:
DISALLOW_COPY_AND_ASSIGN(ContentMismatchUnittest);
};
INSTANTIATE_TEST_SUITE_P(ContentVerifyJobUnittest,
ContentMismatchUnittest,
testing::Values(kNone,
kContentReadBeforeHashesReady,
kHashesReadyBeforeContentRead));
// Tests that content modification causes content verification failure.
TEST_P(ContentMismatchUnittest, ContentMismatch) {
RunContentMismatchTest("console.log('modified');", GetParam());
}
// Similar to ContentMismatch, but uses a file size > 4k.
// Regression test for https://crbug.com/804630.
TEST_P(ContentMismatchUnittest, ContentMismatchWithLargeFile) {
std::string content_larger_than_block_size(
extension_misc::kContentVerificationDefaultBlockSize + 1, ';');
RunContentMismatchTest(content_larger_than_block_size, GetParam());
}
} // namespace extensions