diff options
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/extensions/extension_file_util.cc | 72 | ||||
-rw-r--r-- | chrome/browser/extensions/sandboxed_extension_unpacker.cc | 218 | ||||
-rw-r--r-- | chrome/browser/extensions/sandboxed_extension_unpacker.h | 21 | ||||
-rw-r--r-- | chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc | 160 | ||||
-rw-r--r-- | chrome/browser/utility_process_host.h | 11 | ||||
-rw-r--r-- | chrome/browser/utility_process_host_unittest.cc | 3 |
6 files changed, 396 insertions, 89 deletions
diff --git a/chrome/browser/extensions/extension_file_util.cc b/chrome/browser/extensions/extension_file_util.cc index 52c2c78..e7ecd08 100644 --- a/chrome/browser/extensions/extension_file_util.cc +++ b/chrome/browser/extensions/extension_file_util.cc @@ -16,8 +16,13 @@ #include "chrome/common/json_value_serializer.h" #include "net/base/file_stream.h" +namespace errors = extension_manifest_errors; + namespace extension_file_util { +// Validates locale info. Doesn't check if messages.json files are valid. +static bool ValidateLocaleInfo(const Extension& extension, std::string* error); + const char kInstallDirectoryName[] = "Extensions"; // TODO(mpcomplete): obsolete. remove after migration period. // http://code.google.com/p/chromium/issues/detail?id=19733 @@ -270,6 +275,10 @@ bool ValidateExtension(Extension* extension, std::string* error) { } } + // Validate locale info. + if (!ValidateLocaleInfo(*extension, error)) + return false; + // Check children of extension root to see if any of them start with _ and is // not on the reserved list. if (!CheckForIllegalFilenames(extension->path(), error)) { @@ -382,6 +391,69 @@ ExtensionMessageBundle* LoadLocaleInfo(const FilePath& extension_path, return message_bundle; } +static bool ValidateLocaleInfo(const Extension& extension, std::string* error) { + // default_locale and _locales have to be both present or both missing. + const FilePath path = extension.path().AppendASCII(Extension::kLocaleFolder); + bool path_exists = file_util::PathExists(path); + std::string default_locale = extension.default_locale(); + + // If both default locale and _locales folder are empty, skip verification. + if (!default_locale.empty() || path_exists) { + if (default_locale.empty() && path_exists) { + *error = errors::kLocalesNoDefaultLocaleSpecified; + return false; + } else if (!default_locale.empty() && !path_exists) { + *error = errors::kLocalesTreeMissing; + return false; + } + + // Treat all folders under _locales as valid locales. + file_util::FileEnumerator locales(path, + false, + file_util::FileEnumerator::DIRECTORIES); + + FilePath locale_path = locales.Next(); + if (locale_path.empty()) { + *error = errors::kLocalesTreeMissing; + return false; + } + + const FilePath default_locale_path = path.AppendASCII(default_locale); + bool has_default_locale_message_file = false; + do { + // Skip any strings with '.'. This happens sometimes, for example with + // '.svn' directories. + FilePath relative_path; + if (!extension.path().AppendRelativePath(locale_path, &relative_path)) + NOTREACHED(); + std::wstring subdir(relative_path.ToWStringHack()); + if (std::find(subdir.begin(), subdir.end(), L'.') != subdir.end()) + continue; + + FilePath messages_path = + locale_path.AppendASCII(Extension::kMessagesFilename); + + if (!file_util::PathExists(messages_path)) { + *error = StringPrintf( + "%s %s", errors::kLocalesMessagesFileMissing, + WideToUTF8(messages_path.ToWStringHack()).c_str()); + return false; + } + + if (locale_path == default_locale_path) + has_default_locale_message_file = true; + } while (!(locale_path = locales.Next()).empty()); + + // Only message file for default locale has to exist. + if (!has_default_locale_message_file) { + *error = errors::kLocalesNoDefaultMessages; + return false; + } + } + + return true; +} + bool CheckForIllegalFilenames(const FilePath& extension_path, std::string* error) { // Reserved underscore names. diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.cc b/chrome/browser/extensions/sandboxed_extension_unpacker.cc index 160a0de..65ccded 100644 --- a/chrome/browser/extensions/sandboxed_extension_unpacker.cc +++ b/chrome/browser/extensions/sandboxed_extension_unpacker.cc @@ -21,7 +21,6 @@ #include "chrome/common/extensions/extension_unpacker.h" #include "chrome/common/json_value_serializer.h" #include "net/base/base64.h" - #include "third_party/skia/include/core/SkBitmap.h" const char SandboxedExtensionUnpacker::kExtensionHeaderMagic[] = "Cr24"; @@ -83,7 +82,8 @@ void SandboxedExtensionUnpacker::Start() { // Otherwise, unpack the extension in this process. ExtensionUnpacker unpacker(temp_crx_path); if (unpacker.Run() && unpacker.DumpImagesToFile()) - OnUnpackExtensionSucceeded(*unpacker.parsed_manifest()); + OnUnpackExtensionSucceeded(*unpacker.parsed_manifest(), + *unpacker.parsed_catalogs()); else OnUnpackExtensionFailed(unpacker.error_message()); } @@ -97,38 +97,16 @@ void SandboxedExtensionUnpacker::StartProcessOnIOThread( } void SandboxedExtensionUnpacker::OnUnpackExtensionSucceeded( - const DictionaryValue& manifest) { - DCHECK(ChromeThread::CurrentlyOn(thread_identifier_)); + const DictionaryValue& manifest, + const DictionaryValue& catalogs) { + // Skip check for unittests. + if (thread_identifier_ != ChromeThread::ID_COUNT) + DCHECK(ChromeThread::CurrentlyOn(thread_identifier_)); got_response_ = true; - ExtensionUnpacker::DecodedImages images; - if (!ExtensionUnpacker::ReadImagesFromFile(temp_dir_.path(), &images)) { - ReportFailure("Couldn't read image data from disk."); + scoped_ptr<DictionaryValue> final_manifest(RewriteManifestFile(manifest)); + if (!final_manifest.get()) return; - } - - // Add the public key extracted earlier to the parsed manifest and overwrite - // the original manifest. We do this to ensure the manifest doesn't contain an - // exploitable bug that could be used to compromise the browser. - scoped_ptr<DictionaryValue> final_manifest( - static_cast<DictionaryValue*>(manifest.DeepCopy())); - final_manifest->SetString(extension_manifest_keys::kPublicKey, public_key_); - - std::string manifest_json; - JSONStringValueSerializer serializer(&manifest_json); - serializer.set_pretty_print(true); - if (!serializer.Serialize(*final_manifest)) { - ReportFailure("Error serializing manifest.json."); - return; - } - - FilePath manifest_path = - extension_root_.AppendASCII(Extension::kManifestFilename); - if (!file_util::WriteFile(manifest_path, - manifest_json.data(), manifest_json.size())) { - ReportFailure("Error saving manifest.json."); - return; - } // Create an extension object that refers to the temporary location the // extension was unpacked to. We use this until the extension is finally @@ -144,55 +122,11 @@ void SandboxedExtensionUnpacker::OnUnpackExtensionSucceeded( return; } - // Delete any images that may be used by the browser. We're going to write - // out our own versions of the parsed images, and we want to make sure the - // originals are gone for good. - std::set<FilePath> image_paths = extension_->GetBrowserImages(); - if (image_paths.size() != images.size()) { - ReportFailure("Decoded images don't match what's in the manifest."); + if (!RewriteImageFiles()) return; - } - - for (std::set<FilePath>::iterator it = image_paths.begin(); - it != image_paths.end(); ++it) { - FilePath path = *it; - if (path.IsAbsolute() || path.ReferencesParent()) { - ReportFailure("Invalid path for browser image."); - return; - } - if (!file_util::Delete(extension_root_.Append(path), false)) { - ReportFailure("Error removing old image file."); - return; - } - } - // Write our parsed images back to disk as well. - for (size_t i = 0; i < images.size(); ++i) { - const SkBitmap& image = images[i].a; - FilePath path_suffix = images[i].b; - if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) { - ReportFailure("Invalid path for bitmap image."); - return; - } - FilePath path = extension_root_.Append(path_suffix); - - std::vector<unsigned char> image_data; - // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even - // though they may originally be .jpg, etc. Figure something out. - // http://code.google.com/p/chromium/issues/detail?id=12459 - if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) { - ReportFailure("Error re-encoding theme image."); - return; - } - - // Note: we're overwriting existing files that the utility process wrote, - // so we can be sure the directory exists. - const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]); - if (!file_util::WriteFile(path, image_data_ptr, image_data.size())) { - ReportFailure("Error saving theme image."); - return; - } - } + if (!RewriteCatalogFiles(catalogs)) + return; ReportSuccess(); } @@ -310,3 +244,131 @@ void SandboxedExtensionUnpacker::ReportSuccess() { client_->OnUnpackSuccess(temp_dir_.Take(), extension_root_, extension_.release()); } + +DictionaryValue* SandboxedExtensionUnpacker::RewriteManifestFile( + const DictionaryValue& manifest) { + // Add the public key extracted earlier to the parsed manifest and overwrite + // the original manifest. We do this to ensure the manifest doesn't contain an + // exploitable bug that could be used to compromise the browser. + scoped_ptr<DictionaryValue> final_manifest( + static_cast<DictionaryValue*>(manifest.DeepCopy())); + final_manifest->SetString(extension_manifest_keys::kPublicKey, public_key_); + + std::string manifest_json; + JSONStringValueSerializer serializer(&manifest_json); + serializer.set_pretty_print(true); + if (!serializer.Serialize(*final_manifest)) { + ReportFailure("Error serializing manifest.json."); + return NULL; + } + + FilePath manifest_path = + extension_root_.AppendASCII(Extension::kManifestFilename); + if (!file_util::WriteFile(manifest_path, + manifest_json.data(), manifest_json.size())) { + ReportFailure("Error saving manifest.json."); + return NULL; + } + + return final_manifest.release(); +} + +bool SandboxedExtensionUnpacker::RewriteImageFiles() { + ExtensionUnpacker::DecodedImages images; + if (!ExtensionUnpacker::ReadImagesFromFile(temp_dir_.path(), &images)) { + ReportFailure("Couldn't read image data from disk."); + return false; + } + + // Delete any images that may be used by the browser. We're going to write + // out our own versions of the parsed images, and we want to make sure the + // originals are gone for good. + std::set<FilePath> image_paths = extension_->GetBrowserImages(); + if (image_paths.size() != images.size()) { + ReportFailure("Decoded images don't match what's in the manifest."); + return false; + } + + for (std::set<FilePath>::iterator it = image_paths.begin(); + it != image_paths.end(); ++it) { + FilePath path = *it; + if (path.IsAbsolute() || path.ReferencesParent()) { + ReportFailure("Invalid path for browser image."); + return false; + } + if (!file_util::Delete(extension_root_.Append(path), false)) { + ReportFailure("Error removing old image file."); + return false; + } + } + + // Write our parsed images back to disk as well. + for (size_t i = 0; i < images.size(); ++i) { + const SkBitmap& image = images[i].a; + FilePath path_suffix = images[i].b; + if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) { + ReportFailure("Invalid path for bitmap image."); + return false; + } + FilePath path = extension_root_.Append(path_suffix); + + std::vector<unsigned char> image_data; + // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even + // though they may originally be .jpg, etc. Figure something out. + // http://code.google.com/p/chromium/issues/detail?id=12459 + if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) { + ReportFailure("Error re-encoding theme image."); + return false; + } + + // Note: we're overwriting existing files that the utility process wrote, + // so we can be sure the directory exists. + const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]); + if (!file_util::WriteFile(path, image_data_ptr, image_data.size())) { + ReportFailure("Error saving theme image."); + return false; + } + } + + return true; +} + +bool SandboxedExtensionUnpacker::RewriteCatalogFiles( + const DictionaryValue& catalogs) { + // Write our parsed catalogs back to disk. + DictionaryValue::key_iterator key_it = catalogs.begin_keys(); + for (; key_it != catalogs.end_keys(); ++key_it) { + DictionaryValue* catalog; + if (!catalogs.GetDictionary(*key_it, &catalog)) { + ReportFailure("Invalid catalog data."); + return false; + } + + FilePath relative_path = FilePath::FromWStringHack(*key_it); + relative_path = relative_path.AppendASCII(Extension::kMessagesFilename); + if (relative_path.IsAbsolute() || relative_path.ReferencesParent()) { + ReportFailure("Invalid path for catalog."); + return false; + } + FilePath path = extension_root_.Append(relative_path); + + std::string catalog_json; + JSONStringValueSerializer serializer(&catalog_json); + serializer.set_pretty_print(true); + if (!serializer.Serialize(*catalog)) { + ReportFailure("Error serializing catalog."); + return false; + } + + // Note: we're overwriting existing files that the utility process read, + // so we can be sure the directory exists. + if (!file_util::WriteFile(path, + catalog_json.c_str(), + catalog_json.size())) { + ReportFailure("Error saving catalog."); + return false; + } + } + + return true; +} diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.h b/chrome/browser/extensions/sandboxed_extension_unpacker.h index 06edcbe..9c04c9f 100644 --- a/chrome/browser/extensions/sandboxed_extension_unpacker.h +++ b/chrome/browser/extensions/sandboxed_extension_unpacker.h @@ -20,7 +20,7 @@ class ResourceDispatcherHost; class SandboxedExtensionUnpackerClient : public base::RefCountedThreadSafe<SandboxedExtensionUnpackerClient> { public: - // temp_dir - A temporary directoy containing the results of the extension + // temp_dir - A temporary directory containing the results of the extension // unpacking. The client is responsible for deleting this directory. // // extension_root - The path to the extension root inside of temp_dir. @@ -44,9 +44,9 @@ class SandboxedExtensionUnpackerClient // sources. // // Unpacking an extension using this class makes minor changes to its source, -// such as transcoding all images to PNG and rewriting the manifest JSON. As -// such, it should not be used when the output is not intended to be given back -// to the author. +// such as transcoding all images to PNG, parsing all message catalogs +// and rewriting the manifest JSON. As such, it should not be used when the +// output is not intended to be given back to the author. // // // Lifetime management: @@ -102,6 +102,7 @@ class SandboxedExtensionUnpacker : public UtilityProcessHost::Client { private: class ProcessHostClient; friend class ProcessHostClient; + friend class SandboxedExtensionUnpackerTest; ~SandboxedExtensionUnpacker() {} @@ -120,13 +121,23 @@ class SandboxedExtensionUnpacker : public UtilityProcessHost::Client { void StartProcessOnIOThread(const FilePath& temp_crx_path); // SandboxedExtensionUnpacker - void OnUnpackExtensionSucceeded(const DictionaryValue& manifest); + void OnUnpackExtensionSucceeded(const DictionaryValue& manifest, + const DictionaryValue& catalogs); void OnUnpackExtensionFailed(const std::string& error_message); void OnProcessCrashed(); void ReportFailure(const std::string& message); void ReportSuccess(); + // Overwrites original manifest with safe result from utility process. + // Returns NULL on error. Caller owns the returned object. + DictionaryValue* RewriteManifestFile(const DictionaryValue& manifest); + + // Overwrites original files with safe results from utility process. + // Reports error and returns false if it fails. + bool RewriteImageFiles(); + bool RewriteCatalogFiles(const DictionaryValue& parsed_catalogs); + FilePath crx_path_; ChromeThread::ID thread_identifier_; ResourceDispatcherHost* rdh_; diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc new file mode 100644 index 0000000..93f550d --- /dev/null +++ b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc @@ -0,0 +1,160 @@ +// Copyright (c) 2009 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/file_util.h" +#include "base/path_service.h" +#include "base/ref_counted.h" +#include "base/scoped_temp_dir.h" +#include "base/string_util.h" +#include "base/values.h" +#include "chrome/browser/extensions/sandboxed_extension_unpacker.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/extensions/extension_unpacker.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" + +namespace errors = extension_manifest_errors; +namespace keys = extension_manifest_keys; + +using testing::_; +using testing::Invoke; + +void OnUnpackSuccess(const FilePath& temp_dir, + const FilePath& extension_root, + Extension* extension) { + delete extension; + // Don't delete temp_dir here, we need to do some post op checking. +} + +class MockSandboxedExtensionUnpackerClient + : public SandboxedExtensionUnpackerClient { + public: + virtual ~MockSandboxedExtensionUnpackerClient() {} + + MOCK_METHOD3(OnUnpackSuccess, + void(const FilePath& temp_dir, + const FilePath& extension_root, + Extension* extension)); + + MOCK_METHOD1(OnUnpackFailure, + void(const std::string& error)); + + void DelegateToFake() { + ON_CALL(*this, OnUnpackSuccess(_, _, _)) + .WillByDefault(Invoke(::OnUnpackSuccess)); + } +}; + +class SandboxedExtensionUnpackerTest : public testing::Test { + public: + virtual void SetUp () { + // It will delete itself. + client_ = new MockSandboxedExtensionUnpackerClient; + client_->DelegateToFake(); + } + + virtual void TearDown() { + // Clean up finally. + ASSERT_TRUE(file_util::Delete(install_dir_, true)) << + install_dir_.value(); + } + + void SetupUnpacker(const std::string& crx_name) { + FilePath original_path; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &original_path)); + original_path = original_path.AppendASCII("extensions") + .AppendASCII("unpacker") + .AppendASCII(crx_name); + ASSERT_TRUE(file_util::PathExists(original_path)) << original_path.value(); + + // Try bots won't let us write into DIR_TEST_DATA, so we have to create + // a temp folder to play in. + ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &install_dir_)); + install_dir_ = + install_dir_.AppendASCII("sandboxed_extension_unpacker_test"); + ASSERT_TRUE(file_util::Delete(install_dir_, true)) << + install_dir_.value(); + ASSERT_TRUE(file_util::CreateDirectory(install_dir_)) << + install_dir_.value(); + + FilePath crx_path = install_dir_.AppendASCII(crx_name); + ASSERT_TRUE(file_util::CopyFile(original_path, crx_path)) << + "Original path: " << original_path.value() << + ", Crx path: " << crx_path.value(); + + unpacker_.reset(new ExtensionUnpacker(crx_path)); + + // It will delete itself. + sandboxed_unpacker_ = + new SandboxedExtensionUnpacker(crx_path, NULL, client_); + PrepareUnpackerEnv(); + } + + void PrepareUnpackerEnv() { + sandboxed_unpacker_->extension_root_ = + install_dir_.AppendASCII("TEMP_INSTALL"); + + sandboxed_unpacker_->temp_dir_.Set(install_dir_); + sandboxed_unpacker_->public_key_ = + "ocnapchkplbmjmpfehjocmjnipfmogkh"; + } + + void OnUnpackSucceeded() { + sandboxed_unpacker_->OnUnpackExtensionSucceeded( + *unpacker_->parsed_manifest(), + *unpacker_->parsed_catalogs()); + } + + FilePath GetInstallPath() { + return install_dir_.AppendASCII("TEMP_INSTALL"); + } + + protected: + FilePath install_dir_; + MockSandboxedExtensionUnpackerClient* client_; + scoped_ptr<ExtensionUnpacker> unpacker_; + scoped_refptr<SandboxedExtensionUnpacker> sandboxed_unpacker_; +}; + +TEST_F(SandboxedExtensionUnpackerTest, NoCatalogsSuccess) { + EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _)); + + SetupUnpacker("no_l10n.crx"); + ASSERT_TRUE(unpacker_->Run()); + ASSERT_TRUE(unpacker_->DumpImagesToFile()); + + // Check that there is no _locales folder. + FilePath install_path = + GetInstallPath().AppendASCII(Extension::kLocaleFolder); + EXPECT_FALSE(file_util::PathExists(install_path)); + + OnUnpackSucceeded(); + + // Check that there still is no _locales folder. + EXPECT_FALSE(file_util::PathExists(install_path)); +} + +TEST_F(SandboxedExtensionUnpackerTest, WithCatalogsSuccess) { + EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _)); + + SetupUnpacker("good_l10n.crx"); + ASSERT_TRUE(unpacker_->Run()); + ASSERT_TRUE(unpacker_->DumpImagesToFile()); + + // Delete _locales/en_US/messages.json. + FilePath messages_file; + messages_file = GetInstallPath() + .AppendASCII(Extension::kLocaleFolder) + .AppendASCII("en_US") + .AppendASCII(Extension::kMessagesFilename); + EXPECT_TRUE(file_util::Delete(messages_file, false)); + + OnUnpackSucceeded(); + + // Check that there is _locales/en_US/messages.json file. + EXPECT_TRUE(file_util::PathExists(messages_file)); +} diff --git a/chrome/browser/utility_process_host.h b/chrome/browser/utility_process_host.h index a37a968..773b515 100644 --- a/chrome/browser/utility_process_host.h +++ b/chrome/browser/utility_process_host.h @@ -35,9 +35,11 @@ class UtilityProcessHost : public ChildProcessHost { virtual void OnProcessCrashed() {} // Called when the extension has unpacked successfully. |manifest| is the - // parsed manifest.json file. |images| contains a list of decoded images - // and the associated paths where those images live on disk. - virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest) {} + // parsed manifest.json file. |catalogs| contains list of all parsed + // message catalogs. |images| contains a list of decoded images and the + // associated paths where those images live on disk. + virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest, + const DictionaryValue& catalogs) {} // Called when an error occurred while unpacking the extension. // |error_message| contains a description of the problem. @@ -51,8 +53,7 @@ class UtilityProcessHost : public ChildProcessHost { // Called when an error occurred while parsing the resource data. // |error_message| contains a description of the problem. - virtual void OnUnpackWebResourceFailed( - const std::string& error_message) {} + virtual void OnUnpackWebResourceFailed(const std::string& error_message) {} // Called when an update manifest xml file was successfully parsed. virtual void OnParseUpdateManifestSucceeded( diff --git a/chrome/browser/utility_process_host_unittest.cc b/chrome/browser/utility_process_host_unittest.cc index 950c596..44fb0f8 100644 --- a/chrome/browser/utility_process_host_unittest.cc +++ b/chrome/browser/utility_process_host_unittest.cc @@ -43,7 +43,8 @@ class TestUtilityProcessHostClient : public UtilityProcessHost::Client { NOTREACHED(); } - virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest) { + virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest, + const DictionaryValue& catalogs) { success_ = true; } |