| // Copyright 2021 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/common/utils/content_script_utils.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string_view> |
| |
| #include "base/files/file_util.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "content/public/common/url_constants.h" |
| #include "extensions/common/api/content_scripts.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/manifest_handlers/permissions_parser.h" |
| #include "extensions/common/mojom/match_origin_as_fallback.mojom-shared.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/url_pattern.h" |
| #include "extensions/common/url_pattern_set.h" |
| #include "extensions/common/user_script.h" |
| #include "extensions/strings/grit/extensions_strings.h" |
| #include "net/base/mime_util.h" |
| #include "third_party/blink/public/common/mime_util/mime_util.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| namespace errors = manifest_errors; |
| |
| namespace script_parsing { |
| |
| namespace { |
| |
| size_t g_max_script_length_in_bytes = 1024u * 1024u * 500u; // 500 MB. |
| size_t g_max_scripts_length_per_extension_in_bytes = |
| 1024u * 1024u * 1024u; // 1 GB. |
| |
| const char kEmptyFilesFynamicScriptError[] = |
| "Script with ID '*' must specify at least one js or css file."; |
| constexpr char kEmptyMatchesDynamicScriptError[] = |
| "Script with ID '*' must specify at least one match."; |
| constexpr char kInvalidExcludeMatchDynamicScriptError[] = |
| "Script with ID '*' has invalid value for exclude_matches[*]: *"; |
| constexpr char kInvalidMatchDynamicScriptError[] = |
| "Script with ID '*' has invalid value for matches[*]: *"; |
| constexpr char kForbiddenInlineCodeScriptError[] = |
| "Script with ID '*' has forbidden inline code source"; |
| |
| // Returns true if the given path's mime type can be used for the given content |
| // script type. |
| bool IsMimeTypeValid(const base::FilePath& relative_path, |
| ContentScriptType content_script_type) { |
| auto file_extension = relative_path.Extension(); |
| if (file_extension.empty()) { |
| return false; |
| } |
| |
| // Strip leading ".". |
| file_extension = file_extension.substr(1); |
| |
| // Allow .user.js files, which normally have no mime type, for JS content |
| // scripts. |
| if (content_script_type == ContentScriptType::kJs && |
| base::EqualsCaseInsensitiveASCII(file_extension, |
| UserScript::kFileExtension)) { |
| return true; |
| } |
| |
| // Allow .scss files for CSS content scripts for compatibility with existing |
| // extensions that were using such files before the mime type check was |
| // introduced. |
| if (content_script_type == ContentScriptType::kCss && |
| base::EqualsCaseInsensitiveASCII(file_extension, "scss")) { |
| return true; |
| } |
| |
| std::string mime_type; |
| if (!net::GetWellKnownMimeTypeFromExtension(file_extension, &mime_type)) { |
| return false; |
| } |
| |
| switch (content_script_type) { |
| case ContentScriptType::kJs: |
| return blink::IsSupportedJavascriptMimeType(mime_type); |
| |
| case ContentScriptType::kCss: |
| return mime_type == "text/css"; |
| } |
| } |
| |
| // Returns false and sets the error if script file can't be loaded, or if it's |
| // not UTF-8 encoded. If a script file can be loaded but will exceed |
| // `max_script_length`, return true but add an install warning to `warnings`. |
| // Otherwise, decrement `remaining_length` by the script file's size. |
| bool IsScriptValid(const base::FilePath& path, |
| const base::FilePath& relative_path, |
| size_t max_script_length, |
| int file_not_read_error_id, |
| std::string* error, |
| std::vector<InstallWarning>* warnings, |
| size_t& remaining_length) { |
| InstallWarning script_file_too_large_warning( |
| l10n_util::GetStringFUTF8(IDS_EXTENSION_CONTENT_SCRIPT_FILE_TOO_LARGE, |
| relative_path.LossyDisplayName()), |
| api::content_scripts::ManifestKeys::kContentScripts, |
| base::UTF16ToUTF8(relative_path.LossyDisplayName())); |
| if (remaining_length == 0u) { |
| warnings->push_back(std::move(script_file_too_large_warning)); |
| return true; |
| } |
| |
| if (!base::PathExists(path)) { |
| *error = l10n_util::GetStringFUTF8(file_not_read_error_id, |
| relative_path.LossyDisplayName()); |
| return false; |
| } |
| |
| std::string content; |
| bool read_successful = |
| base::ReadFileToStringWithMaxSize(path, &content, max_script_length); |
| // If the size of the file in `path` exceeds `max_script_length`, |
| // ReadFileToStringWithMaxSize will return false but `content` will contain |
| // the file's content truncated to `max_script_length`. |
| if (!read_successful && content.length() != max_script_length) { |
| *error = l10n_util::GetStringFUTF8(file_not_read_error_id, |
| relative_path.LossyDisplayName()); |
| return false; |
| } |
| |
| if (!base::IsStringUTF8(content)) { |
| *error = l10n_util::GetStringFUTF8(IDS_EXTENSION_BAD_FILE_ENCODING, |
| relative_path.LossyDisplayName()); |
| return false; |
| } |
| |
| if (read_successful) { |
| remaining_length -= content.size(); |
| } else { |
| // Even though the script file is over the max size, we don't throw a hard |
| // error so as not to break any existing extensions for which this is the |
| // case. |
| warnings->push_back(std::move(script_file_too_large_warning)); |
| } |
| return true; |
| } |
| |
| // Returns a string error when dynamic script with `script_id` or static script |
| // at `definition_index` has an empty field error. |
| std::u16string GetEmptyFieldError(std::string_view static_error, |
| std::string_view dynamic_error, |
| const std::string& script_id, |
| std::optional<int> definition_index) { |
| // Static scripts use a manifest error with `definition_index` since the |
| // script id is autogenerated and the caller is unaware of it. |
| if (definition_index) { |
| return ErrorUtils::FormatErrorMessageUTF16( |
| static_error, base::NumberToString(*definition_index)); |
| } |
| |
| return ErrorUtils::FormatErrorMessageUTF16( |
| dynamic_error, UserScript::TrimPrefixFromScriptID(script_id)); |
| } |
| |
| // Returns a string error when dynamic script with `dynamic_error` and |
| // `script_id`, or static script with `static_error` at `definition_index` has |
| // an invalid exclude matches error. |
| std::u16string GetInvalidMatchError(std::string_view static_error, |
| std::string_view dynamic_error, |
| const std::string& script_id, |
| std::optional<int> definition_index, |
| URLPattern::ParseResult parse_result, |
| int match_index) { |
| std::string match_index_string = base::NumberToString(match_index); |
| std::string parse_result_string = |
| URLPattern::GetParseResultString(parse_result); |
| |
| // Static scripts use a manifest error with `definition_index` since the |
| // script id is autogenerated and the caller is unaware of it. |
| if (definition_index) { |
| return ErrorUtils::FormatErrorMessageUTF16( |
| static_error, base::NumberToString(*definition_index), |
| match_index_string, parse_result_string); |
| } |
| |
| return ErrorUtils::FormatErrorMessageUTF16( |
| dynamic_error, UserScript::TrimPrefixFromScriptID(script_id), |
| match_index_string, parse_result_string); |
| } |
| |
| // Returns a string error when dynamic script with `script_id` or static script |
| // at `definition_index` has an empty matches error. |
| std::u16string GetEmptyMatchesError(const std::string& script_id, |
| std::optional<int> definition_index) { |
| return GetEmptyFieldError(errors::kInvalidMatchCount, |
| kEmptyMatchesDynamicScriptError, script_id, |
| definition_index); |
| } |
| |
| // Returns a string error when dynamic script with `script_id` or static script |
| // at `definition_index` has an empty files error. |
| std::u16string GetEmptyFilesError(const std::string& script_id, |
| std::optional<int> definition_index) { |
| return GetEmptyFieldError(errors::kMissingFile, kEmptyFilesFynamicScriptError, |
| script_id, definition_index); |
| } |
| |
| // Returns a string error when dynamic script with `script_id` or static script |
| // at `definition_index` has an invalid exclude matches error. |
| std::u16string GetInvalidExcludeMatchError(const std::string& script_id, |
| std::optional<int> definition_index, |
| URLPattern::ParseResult parse_result, |
| int match_index) { |
| return GetInvalidMatchError(errors::kInvalidExcludeMatch, |
| kInvalidExcludeMatchDynamicScriptError, script_id, |
| definition_index, parse_result, match_index); |
| } |
| |
| // Returns a string error when dynamic script with `script_id` or static script |
| // at `definition_index` has an invalid matches error. |
| std::u16string GetInvalidMatchError(const std::string& script_id, |
| std::optional<int> definition_index, |
| URLPattern::ParseResult parse_result, |
| int match_index) { |
| return GetInvalidMatchError(errors::kInvalidMatch, |
| kInvalidMatchDynamicScriptError, script_id, |
| definition_index, parse_result, match_index); |
| } |
| |
| } // namespace |
| |
| size_t GetMaxScriptLength() { |
| return g_max_script_length_in_bytes; |
| } |
| |
| size_t GetMaxScriptsLengthPerExtension() { |
| return g_max_scripts_length_per_extension_in_bytes; |
| } |
| |
| ScopedMaxScriptLengthOverride CreateScopedMaxScriptLengthForTesting( // IN-TEST |
| size_t max) { |
| return ScopedMaxScriptLengthOverride(&g_max_script_length_in_bytes, max); |
| } |
| |
| ScopedMaxScriptLengthOverride |
| CreateScopedMaxScriptsLengthPerExtensionForTesting(size_t max) { |
| return ScopedMaxScriptLengthOverride( |
| &g_max_scripts_length_per_extension_in_bytes, max); |
| } |
| |
| bool ParseMatchPatterns(const std::vector<std::string>& matches, |
| const std::vector<std::string>* exclude_matches, |
| int creation_flags, |
| bool can_execute_script_everywhere, |
| bool all_urls_includes_chrome_urls, |
| std::optional<int> definition_index, |
| UserScript* result, |
| std::u16string* error, |
| bool* wants_file_access) { |
| if (matches.empty()) { |
| *error = GetEmptyMatchesError(result->id(), definition_index); |
| return false; |
| } |
| |
| const int valid_schemes = |
| UserScript::ValidUserScriptSchemes(can_execute_script_everywhere); |
| |
| for (size_t i = 0; i < matches.size(); ++i) { |
| URLPattern pattern(valid_schemes); |
| |
| const std::string& match_str = matches[i]; |
| URLPattern::ParseResult parse_result = pattern.Parse(match_str); |
| if (parse_result != URLPattern::ParseResult::kSuccess) { |
| *error = |
| GetInvalidMatchError(result->id(), definition_index, parse_result, i); |
| return false; |
| } |
| |
| // TODO(aboxhall): check for webstore |
| if (!all_urls_includes_chrome_urls && |
| pattern.scheme() != content::kChromeUIScheme) { |
| // Exclude SCHEME_CHROMEUI unless it's been explicitly requested or |
| // been granted by extension ID. |
| // If the --extensions-on-chrome-urls flag has not been passed, requesting |
| // a chrome:// url will cause a parse failure above, so there's no need to |
| // check the flag here. |
| pattern.SetValidSchemes(pattern.valid_schemes() & |
| ~URLPattern::SCHEME_CHROMEUI); |
| } |
| |
| if (pattern.MatchesScheme(url::kFileScheme) && |
| !can_execute_script_everywhere) { |
| if (wants_file_access) |
| *wants_file_access = true; |
| if (!(creation_flags & Extension::ALLOW_FILE_ACCESS)) { |
| pattern.SetValidSchemes(pattern.valid_schemes() & |
| ~URLPattern::SCHEME_FILE); |
| } |
| } |
| |
| result->add_url_pattern(pattern); |
| } |
| |
| if (exclude_matches) { |
| for (size_t i = 0; i < exclude_matches->size(); ++i) { |
| const std::string& match_str = exclude_matches->at(i); |
| URLPattern pattern(valid_schemes); |
| |
| URLPattern::ParseResult parse_result = pattern.Parse(match_str); |
| if (parse_result != URLPattern::ParseResult::kSuccess) { |
| *error = GetInvalidExcludeMatchError(result->id(), definition_index, |
| parse_result, i); |
| return false; |
| } |
| |
| result->add_exclude_url_pattern(pattern); |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ParseFileSources( |
| const Extension* extension, |
| const std::vector<api::scripts_internal::ScriptSource>* js, |
| const std::vector<api::scripts_internal::ScriptSource>* css, |
| std::optional<int> definition_index, |
| UserScript* result, |
| std::u16string* error) { |
| if (js) { |
| result->js_scripts().reserve(js->size()); |
| for (const auto& source : *js) { |
| if (source.file) { |
| GURL url = extension->GetResourceURL(base::EscapePath(*source.file)); |
| ExtensionResource resource = extension->GetResource(*source.file); |
| result->js_scripts().push_back(UserScript::Content::CreateFile( |
| resource.extension_root(), resource.relative_path(), url)); |
| } else if (source.code) { |
| // Inline code source is only allowed for user scripts. |
| if (result->GetSource() != UserScript::Source::kDynamicUserScript) { |
| *error = ErrorUtils::FormatErrorMessageUTF16( |
| kForbiddenInlineCodeScriptError, |
| UserScript::TrimPrefixFromScriptID(result->id())); |
| return false; |
| } |
| |
| GURL url = extension->GetResourceURL( |
| base::Uuid::GenerateRandomV4().AsLowercaseString()); |
| std::unique_ptr<UserScript::Content> content = |
| UserScript::Content::CreateInlineCode(url); |
| // TODO(crbug.com/40938420): This creates a copy of a |
| // potentially-expensive string. Optimize the usage of inline code. |
| content->set_content(*source.code); |
| result->js_scripts().push_back(std::move(content)); |
| } |
| } |
| } |
| |
| if (css) { |
| result->css_scripts().reserve(css->size()); |
| for (const auto& source : *css) { |
| if (source.file) { |
| GURL url = extension->GetResourceURL(base::EscapePath(*source.file)); |
| ExtensionResource resource = extension->GetResource(*source.file); |
| result->css_scripts().push_back(UserScript::Content::CreateFile( |
| resource.extension_root(), resource.relative_path(), url)); |
| } |
| // Note: We don't allow `code` in CSS blocks of any user script types yet. |
| } |
| } |
| |
| // The manifest needs to have at least one js or css user script definition. |
| if (result->js_scripts().empty() && result->css_scripts().empty()) { |
| *error = GetEmptyFilesError(result->id(), definition_index); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void ParseGlobs(const std::vector<std::string>* include_globs, |
| const std::vector<std::string>* exclude_globs, |
| UserScript* result) { |
| if (include_globs) { |
| for (const std::string& glob : *include_globs) { |
| result->add_glob(glob); |
| } |
| } |
| |
| if (exclude_globs) { |
| for (const std::string& glob : *exclude_globs) { |
| result->add_exclude_glob(glob); |
| } |
| } |
| } |
| |
| bool ValidateMimeTypeFromFileExtension(const base::FilePath& relative_path, |
| ContentScriptType content_script_type, |
| std::string* error) { |
| if (!IsMimeTypeValid(relative_path, content_script_type)) { |
| switch (content_script_type) { |
| case ContentScriptType::kJs: |
| *error = l10n_util::GetStringFUTF8( |
| IDS_EXTENSION_CONTENT_SCRIPT_FILE_BAD_JS_MIME_TYPE, |
| relative_path.LossyDisplayName()); |
| return false; |
| |
| case ContentScriptType::kCss: |
| *error = l10n_util::GetStringFUTF8( |
| IDS_EXTENSION_CONTENT_SCRIPT_FILE_BAD_CSS_MIME_TYPE, |
| relative_path.LossyDisplayName()); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateFileSources(const UserScriptList& scripts, |
| ExtensionResource::SymlinkPolicy symlink_policy, |
| std::string* error, |
| std::vector<InstallWarning>* warnings) { |
| size_t remaining_scripts_length = GetMaxScriptsLengthPerExtension(); |
| |
| for (const std::unique_ptr<UserScript>& script : scripts) { |
| for (const std::unique_ptr<UserScript::Content>& js_script : |
| script->js_scripts()) { |
| // Don't validate scripts with inline code source, since they don't have |
| // file sources. |
| if (js_script->source() == UserScript::Content::Source::kInlineCode) { |
| continue; |
| } |
| |
| // Files with invalid mime types will be ignored. |
| if (!ValidateMimeTypeFromFileExtension(js_script->relative_path(), |
| ContentScriptType::kJs, error)) { |
| warnings->emplace_back( |
| base::StringPrintf(errors::kInvalidUserScriptMimeType, |
| error->c_str()), |
| api::content_scripts::ManifestKeys::kContentScripts); |
| continue; |
| } |
| |
| const base::FilePath& path = ExtensionResource::GetFilePath( |
| js_script->extension_root(), js_script->relative_path(), |
| symlink_policy); |
| size_t max_script_length = |
| std::min(remaining_scripts_length, GetMaxScriptLength()); |
| if (!IsScriptValid(path, js_script->relative_path(), max_script_length, |
| IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, error, warnings, |
| remaining_scripts_length)) { |
| return false; |
| } |
| } |
| |
| for (const std::unique_ptr<UserScript::Content>& css_script : |
| script->css_scripts()) { |
| // Files with invalid mime types will be ignored. |
| if (!ValidateMimeTypeFromFileExtension(css_script->relative_path(), |
| ContentScriptType::kCss, error)) { |
| warnings->emplace_back( |
| base::StringPrintf(errors::kInvalidUserScriptMimeType, |
| error->c_str()), |
| api::content_scripts::ManifestKeys::kContentScripts); |
| continue; |
| } |
| |
| const base::FilePath& path = ExtensionResource::GetFilePath( |
| css_script->extension_root(), css_script->relative_path(), |
| symlink_policy); |
| size_t max_script_length = |
| std::min(remaining_scripts_length, GetMaxScriptLength()); |
| if (!IsScriptValid(path, css_script->relative_path(), max_script_length, |
| IDS_EXTENSION_LOAD_CSS_FAILED, error, warnings, |
| remaining_scripts_length)) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateUserScriptMimeTypesFromFileExtensions(const UserScript& script, |
| std::string* error) { |
| for (const std::unique_ptr<UserScript::Content>& js_script : |
| script.js_scripts()) { |
| // Don't validate scripts with inline code source, since they don't have |
| // file sources. |
| if (js_script->source() == UserScript::Content::Source::kInlineCode) { |
| continue; |
| } |
| |
| if (!ValidateMimeTypeFromFileExtension(js_script->relative_path(), |
| ContentScriptType::kJs, error)) { |
| return false; |
| } |
| } |
| |
| for (const std::unique_ptr<UserScript::Content>& css_script : |
| script.css_scripts()) { |
| if (!ValidateMimeTypeFromFileExtension(css_script->relative_path(), |
| ContentScriptType::kCss, error)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateMatchOriginAsFallback( |
| mojom::MatchOriginAsFallbackBehavior match_origin_as_fallback, |
| const URLPatternSet& url_patterns, |
| std::u16string* error_out) { |
| // If the extension is using `match_origin_as_fallback`, we require the |
| // pattern to match all paths. This is because origins don't have a path; |
| // thus, if an extension specified `"match_origin_as_fallback": true` for |
| // a pattern of `"https://google.com/maps/*"`, this script would also run |
| // on about:blank, data:, etc frames from https://google.com (because in |
| // both cases, the precursor origin is https://google.com). |
| if (match_origin_as_fallback == |
| mojom::MatchOriginAsFallbackBehavior::kAlways) { |
| for (const auto& pattern : url_patterns) { |
| if (pattern.path() != "/*") { |
| *error_out = errors::kMatchOriginAsFallbackCantHavePaths; |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| ExtensionResource::SymlinkPolicy GetSymlinkPolicy(const Extension* extension) { |
| if ((extension->creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE) != |
| 0) { |
| return ExtensionResource::FOLLOW_SYMLINKS_ANYWHERE; |
| } |
| |
| return ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT; |
| } |
| |
| } // namespace script_parsing |
| } // namespace extensions |