// 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/browser/scripting_utils.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/web_contents.h" #include "extensions/browser/extension_file_task_runner.h" #include "extensions/browser/extension_prefs.h" #include "extensions/browser/extension_system.h" #include "extensions/browser/extension_util.h" #include "extensions/browser/extensions_browser_client.h" #include "extensions/browser/load_and_localize_file.h" #include "extensions/browser/script_executor.h" #include "extensions/browser/scripting_constants.h" #include "extensions/browser/user_script_manager.h" #include "extensions/common/api/scripts_internal.h" #include "extensions/common/error_utils.h" #include "extensions/common/manifest_constants.h" #include "extensions/common/permissions/permissions_data.h" #include "extensions/common/user_script.h" #include "extensions/common/utils/content_script_utils.h" namespace extensions::scripting { namespace { constexpr char kEmptyScriptIdError[] = "Script's ID must not be empty"; constexpr char kFilesExceededSizeLimitError[] = "Scripts could not be loaded because '*' exceeds the maximum script size " "or the extension's maximum total script size."; constexpr char kCouldNotLoadFileError[] = "Could not load file: '*'."; constexpr char kDuplicateFileSpecifiedError[] = "Duplicate file specified: '*'."; constexpr char kNonExistentScriptIdError[] = "Nonexistent script ID '*'"; // Key corresponding to the set of URL patterns from the extension's persistent // dynamic content scripts. constexpr const char kPrefPersistentScriptURLPatterns[] = "persistent_script_url_patterns"; constexpr char kReservedScriptIdPrefixError[] = "Script's ID '*' must not start with '*'"; constexpr char kInvalidTabIdError[] = "No tab with id: *"; constexpr char kInvalidDocumentIdError[] = "Invalid document id *"; constexpr char kInvalidDocumentIdForTabError[] = "No document with id * in tab with id *"; constexpr char kInvalidFrameIdError[] = "No frame with id * in tab with id *"; constexpr char kInvalidAllFramesTargetError[] = "Cannot specify 'allFrames' if either 'frameIds' or 'documentIds' is " "specified."; constexpr char kInvalidTargetIdsError[] = "Cannot specify both 'frameIds' and 'documentIds'."; // Returns an error message string for when an extension cannot access a page it // is attempting to. std::string GetCannotAccessPageErrorMessage(const PermissionsData& permissions, const GURL& url) { if (permissions.HasAPIPermission(mojom::APIPermissionID::kTab)) { return ErrorUtils::FormatErrorMessage( manifest_errors::kCannotAccessPageWithUrl, url.spec()); } return manifest_errors::kCannotAccessPage; } // Collects the frames for injection. Method will return false if an error is // encountered. bool CollectFramesForInjection(const scripting::InjectionTarget& target, content::WebContents* tab, std::set<int>& frame_ids, std::set<content::RenderFrameHost*>& frames, std::string* error_out) { if (target.document_ids) { for (const auto& id : *target.document_ids) { ExtensionApiFrameIdMap::DocumentId document_id = ExtensionApiFrameIdMap::DocumentIdFromString(id); if (!document_id) { *error_out = ErrorUtils::FormatErrorMessage(kInvalidDocumentIdError, id.c_str()); return false; } content::RenderFrameHost* frame = ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId( document_id); // If the frame was not found or it matched another tab reject this // request. if (!frame || content::WebContents::FromRenderFrameHost(frame) != tab) { *error_out = ErrorUtils::FormatErrorMessage( kInvalidDocumentIdForTabError, id.c_str(), base::NumberToString(target.tab_id)); return false; } // Convert the documentId into a frameId since the content will be // injected synchronously. frame_ids.insert(ExtensionApiFrameIdMap::GetFrameId(frame)); frames.insert(frame); } } else { if (target.frame_ids) { frame_ids.insert(target.frame_ids->begin(), target.frame_ids->end()); } else { frame_ids.insert(ExtensionApiFrameIdMap::kTopFrameId); } for (int frame_id : frame_ids) { content::RenderFrameHost* frame = ExtensionApiFrameIdMap::GetRenderFrameHostById(tab, frame_id); if (!frame) { *error_out = ErrorUtils::FormatErrorMessage( kInvalidFrameIdError, base::NumberToString(frame_id), base::NumberToString(target.tab_id)); return false; } frames.insert(frame); } } return true; } // Returns true if the `permissions` allow for injection into the given `frame`. // If false, populates `error`. bool HasPermissionToInjectIntoFrame(const PermissionsData& permissions, int tab_id, content::RenderFrameHost* frame, std::string* error) { GURL committed_url = frame->GetLastCommittedURL(); if (committed_url.is_empty()) { if (!frame->IsInPrimaryMainFrame()) { // We can't check the pending URL for subframes from the //chrome layer. // Assume the injection is allowed; the renderer has additional checks // later on. return true; } // Unknown URL, e.g. because no load was committed yet. In this case we look // for any pending entry on the NavigationController associated with the // WebContents for the frame. content::WebContents* web_contents = content::WebContents::FromRenderFrameHost(frame); content::NavigationEntry* pending_entry = web_contents->GetController().GetPendingEntry(); if (!pending_entry) { *error = manifest_errors::kCannotAccessPage; return false; } GURL pending_url = pending_entry->GetURL(); if (pending_url.SchemeIsHTTPOrHTTPS() && !permissions.CanAccessPage(pending_url, tab_id, error)) { // This catches the majority of cases where an extension tried to inject // on a newly-created navigating tab, saving us a potentially-costly IPC // and, maybe, slightly reducing (but not by any stretch eliminating) an // attack surface. *error = GetCannotAccessPageErrorMessage(permissions, pending_url); return false; } // Otherwise allow for now. The renderer has additional checks and will // fail the injection if needed. return true; } // TODO(devlin): Add more schemes here, in line with // https://crbug.com/55084. if (committed_url.SchemeIs(url::kAboutScheme) || committed_url.SchemeIs(url::kDataScheme)) { url::Origin origin = frame->GetLastCommittedOrigin(); const url::SchemeHostPort& tuple_or_precursor_tuple = origin.GetTupleOrPrecursorTupleIfOpaque(); if (!tuple_or_precursor_tuple.IsValid()) { *error = GetCannotAccessPageErrorMessage(permissions, committed_url); return false; } committed_url = tuple_or_precursor_tuple.GetURL(); } return permissions.CanAccessPage(committed_url, tab_id, error); } // Constructs an array of file sources from the read file `data`. std::vector<InjectedFileSource> ConstructFileSources( std::vector<std::unique_ptr<std::string>> data, std::vector<std::string> file_names) { // Note: CHECK (and not DCHECK) because if it fails, we have an out-of-bounds // access. CHECK_EQ(data.size(), file_names.size()); const size_t num_sources = data.size(); std::vector<InjectedFileSource> sources; sources.reserve(num_sources); for (size_t i = 0; i < num_sources; ++i) { sources.emplace_back(std::move(file_names[i]), std::move(data[i])); } return sources; } // Checks the loaded content of extension resources. Invokes `callback` with // the constructed file sources on success or with an error on failure. void CheckLoadedResources(std::vector<std::string> file_names, ResourcesLoadedCallback callback, std::vector<std::unique_ptr<std::string>> file_data, std::optional<std::string> load_error) { if (load_error) { std::move(callback).Run({}, std::move(load_error)); return; } std::vector<InjectedFileSource> file_sources = ConstructFileSources(std::move(file_data), std::move(file_names)); for (const auto& source : file_sources) { DCHECK(source.data); // TODO(devlin): What necessitates this encoding requirement? Is it needed // for blink injection? if (!base::IsStringUTF8(*source.data)) { static constexpr char kBadFileEncodingError[] = "Could not load file '*'. It isn't UTF-8 encoded."; std::string error = ErrorUtils::FormatErrorMessage(kBadFileEncodingError, source.file_name); std::move(callback).Run({}, std::move(error)); return; } } std::move(callback).Run(std::move(file_sources), std::nullopt); } } // namespace InjectionTarget::InjectionTarget() : tab_id(-1) {} InjectionTarget::InjectionTarget(InjectionTarget&& other) = default; InjectionTarget::~InjectionTarget() = default; InjectedFileSource::InjectedFileSource(std::string file_name, std::unique_ptr<std::string> data) : file_name(std::move(file_name)), data(std::move(data)) {} InjectedFileSource::InjectedFileSource(InjectedFileSource&&) = default; InjectedFileSource::~InjectedFileSource() = default; std::string AddPrefixToDynamicScriptId(const std::string& script_id, UserScript::Source source) { std::string prefix; switch (source) { case UserScript::Source::kDynamicContentScript: prefix = UserScript::kDynamicContentScriptPrefix; break; case UserScript::Source::kDynamicUserScript: prefix = UserScript::kDynamicUserScriptPrefix; break; case UserScript::Source::kStaticContentScript: case UserScript::Source::kWebUIScript: NOTREACHED(); } return prefix + script_id; } bool IsScriptIdValid(const std::string& script_id, std::string* error) { if (script_id.empty()) { *error = kEmptyScriptIdError; return false; } if (script_id[0] == UserScript::kReservedScriptIDPrefix) { *error = ErrorUtils::FormatErrorMessage( kReservedScriptIdPrefixError, script_id, std::string(1, UserScript::kReservedScriptIDPrefix)); return false; } return true; } bool ScriptsShouldBeAllowedInIncognito( const ExtensionId& extension_id, content::BrowserContext* browser_context) { // Note: We explicitly use `util::IsIncognitoEnabled()` (and not // `ExtensionFunction::include_incognito_information()`) since the latter // excludes the on-the-record context of a split-mode extension. Since user // scripts are shared across profiles, we should use the overall setting for // the extension. return util::IsIncognitoEnabled(extension_id, browser_context); } bool RemoveScripts( const std::optional<std::vector<std::string>>& ids, UserScript::Source source, content::BrowserContext* browser_context, const ExtensionId& extension_id, ExtensionUserScriptLoader::DynamicScriptsModifiedCallback remove_callback, std::string* error) { ExtensionUserScriptLoader* loader = ExtensionSystem::Get(browser_context) ->user_script_manager() ->GetUserScriptLoaderForExtension(extension_id); // Remove all scripts if ids are not provided. This doesn't include when ids // has a value, but it's empty. if (!ids.has_value()) { loader->ClearDynamicScripts(source, std::move(remove_callback)); return true; } std::set<std::string> ids_to_remove; std::set<std::string> existing_script_ids = loader->GetDynamicScriptIDs(source); for (const auto& id : *ids) { if (!scripting::IsScriptIdValid(id, error)) { return false; } // Add the dynamic script prefix to `provided_id` before checking against // `existing_script_ids`. std::string id_with_prefix = scripting::AddPrefixToDynamicScriptId(id, source); if (!base::Contains(existing_script_ids, id_with_prefix)) { *error = ErrorUtils::FormatErrorMessage(kNonExistentScriptIdError, id.c_str()); return false; } ids_to_remove.insert(id_with_prefix); } loader->RemoveDynamicScripts(std::move(ids_to_remove), std::move(remove_callback)); return true; } URLPatternSet GetPersistentScriptURLPatterns( content::BrowserContext* browser_context, const ExtensionId& extension_id) { URLPatternSet patterns; ExtensionPrefs::Get(browser_context) ->ReadPrefAsURLPatternSet(extension_id, kPrefPersistentScriptURLPatterns, &patterns, UserScript::ValidUserScriptSchemes()); return patterns; } void SetPersistentScriptURLPatterns(content::BrowserContext* browser_context, const ExtensionId& extension_id, const URLPatternSet& patterns) { ExtensionPrefs::Get(browser_context) ->SetExtensionPrefURLPatternSet( extension_id, kPrefPersistentScriptURLPatterns, patterns); } void ClearPersistentScriptURLPatterns(content::BrowserContext* browser_context, const ExtensionId& extension_id) { ExtensionPrefs::Get(browser_context) ->UpdateExtensionPref(extension_id, kPrefPersistentScriptURLPatterns, std::nullopt); } ValidateScriptsResult ValidateParsedScriptsOnFileThread( ExtensionResource::SymlinkPolicy symlink_policy, UserScriptList scripts) { DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); // Validate that claimed script resources actually exist, and are UTF-8 // encoded. std::string error; std::vector<InstallWarning> warnings; bool are_script_files_valid = script_parsing::ValidateFileSources( scripts, symlink_policy, &error, &warnings); // Script files over the per script/extension size limit are recorded as // warnings. However, for this case we should treat "install warnings" as // errors by turning this call into a no-op and returning an error. if (!warnings.empty() && error.empty()) { error = ErrorUtils::FormatErrorMessage(kFilesExceededSizeLimitError, warnings[0].specific); are_script_files_valid = false; } return std::make_pair(std::move(scripts), are_script_files_valid ? std::nullopt : std::make_optional(error)); } bool CanAccessTarget(const PermissionsData& permissions, const scripting::InjectionTarget& target, content::BrowserContext* browser_context, bool include_incognito_information, ScriptExecutor** script_executor_out, ScriptExecutor::FrameScope* frame_scope_out, std::set<int>* frame_ids_out, std::string* error_out) { ExtensionsBrowserClient* browser_client = ExtensionsBrowserClient::Get(); content::WebContents* web_contents = nullptr; if (!browser_client->IsValidTabId(browser_context, target.tab_id, include_incognito_information, &web_contents)) { *error_out = ErrorUtils::FormatErrorMessage( kInvalidTabIdError, base::NumberToString(target.tab_id)); return false; } ScriptExecutor* script_executor = browser_client->GetScriptExecutorForTab(*web_contents); if (!script_executor) { *error_out = ErrorUtils::FormatErrorMessage( kInvalidTabIdError, base::NumberToString(target.tab_id)); return false; } if (target.all_frames.value_or(false) && (target.frame_ids || target.document_ids)) { *error_out = kInvalidAllFramesTargetError; return false; } if (target.frame_ids && target.document_ids) { *error_out = kInvalidTargetIdsError; return false; } ScriptExecutor::FrameScope frame_scope = target.all_frames.value_or(false) ? ScriptExecutor::INCLUDE_SUB_FRAMES : ScriptExecutor::SPECIFIED_FRAMES; std::set<int> frame_ids; std::set<content::RenderFrameHost*> frames; if (!CollectFramesForInjection(target, web_contents, frame_ids, frames, error_out)) { return false; } // TODO(devlin): If `allFrames` is true, we error out if the extension // doesn't have access to the top frame (even if it may inject in child // frames). This is inconsistent with content scripts (which can execute // on child frames), but consistent with the old tabs.executeScript() API. for (content::RenderFrameHost* frame : frames) { DCHECK_EQ(content::WebContents::FromRenderFrameHost(frame), web_contents); if (!HasPermissionToInjectIntoFrame(permissions, target.tab_id, frame, error_out)) { return false; } } *frame_ids_out = std::move(frame_ids); *frame_scope_out = frame_scope; *script_executor_out = script_executor; return true; } bool CheckAndLoadFiles(std::vector<std::string> files, const Extension& extension, bool requires_localization, ResourcesLoadedCallback callback, std::string* error_out) { std::vector<ExtensionResource> resources; if (!GetFileResources(files, extension, &resources, error_out)) { return false; } LoadAndLocalizeResources( extension, resources, requires_localization, script_parsing::GetMaxScriptLength(), base::BindOnce(&CheckLoadedResources, std::move(files), std::move(callback))); return true; } bool GetFileResources(const std::vector<std::string>& files, const Extension& extension, std::vector<ExtensionResource>* resources_out, std::string* error_out) { if (files.empty()) { static constexpr char kAtLeastOneFileError[] = "At least one file must be specified."; *error_out = kAtLeastOneFileError; return false; } std::vector<ExtensionResource> resources; for (const auto& file : files) { ExtensionResource resource = extension.GetResource(file); if (resource.extension_root().empty() || resource.relative_path().empty()) { *error_out = ErrorUtils::FormatErrorMessage(kCouldNotLoadFileError, file); return false; } // ExtensionResource doesn't implement an operator==. if (base::Contains(resources, resource.relative_path(), &ExtensionResource::relative_path)) { // Disallow duplicates. Note that we could allow this, if we wanted (and // there *might* be reason to with JS injection, to perform an operation // twice?). However, this matches content script behavior, and injecting // twice can be done by chaining calls to executeScript() / insertCSS(). // This isn't a robust check, and could probably be circumvented by // passing two paths that look different but are the same - but in that // case, we just try to load and inject the script twice, which is // inefficient, but safe. *error_out = ErrorUtils::FormatErrorMessage(kDuplicateFileSpecifiedError, file); return false; } resources.push_back(std::move(resource)); } resources_out->swap(resources); return true; } void ExecuteScript(const ExtensionId& extension_id, std::vector<mojom::JSSourcePtr> sources, mojom::ExecutionWorld execution_world, const std::optional<std::string>& world_id, ScriptExecutor* script_executor, ScriptExecutor::FrameScope frame_scope, std::set<int> frame_ids, bool inject_immediately, bool user_gesture, ScriptExecutor::ScriptFinishedCallback callback) { // Extensions can specify that the script should be injected "immediately". // In this case, we specify kDocumentStart as the injection time. Due to // inherent raciness between tab creation and load and this function // execution, there is no guarantee that it will actually happen at // document start, but the renderer will appropriately inject it // immediately if document start has already passed. mojom::RunLocation run_location = inject_immediately ? mojom::RunLocation::kDocumentStart : mojom::RunLocation::kDocumentIdle; script_executor->ExecuteScript( mojom::HostID(mojom::HostID::HostType::kExtensions, extension_id), mojom::CodeInjection::NewJs(mojom::JSInjection::New( std::move(sources), execution_world, world_id, blink::mojom::WantResultOption::kWantResult, user_gesture ? blink::mojom::UserActivationOption::kActivate : blink::mojom::UserActivationOption::kDoNotActivate, blink::mojom::PromiseResultOption::kAwait)), frame_scope, frame_ids, mojom::MatchOriginAsFallbackBehavior::kAlways, run_location, ScriptExecutor::DEFAULT_PROCESS, /*webview_src=*/GURL(), std::move(callback)); } } // namespace extensions::scripting