// Copyright 2013 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/renderer/extension_frame_helper.h" #include #include "base/metrics/histogram_macros.h" #include "base/ranges/algorithm.h" #include "base/strings/string_util.h" #include "base/timer/elapsed_timer.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_view.h" #include "extensions/common/api/messaging/message.h" #include "extensions/common/api/messaging/port_id.h" #include "extensions/common/constants.h" #include "extensions/common/extension_messages.h" #include "extensions/common/manifest_handlers/background_info.h" #include "extensions/renderer/api/automation/automation_api_helper.h" #include "extensions/renderer/console.h" #include "extensions/renderer/dispatcher.h" #include "extensions/renderer/native_extension_bindings_system.h" #include "extensions/renderer/native_renderer_messaging_service.h" #include "extensions/renderer/script_context.h" #include "extensions/renderer/script_context_set.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" #include "third_party/blink/public/platform/web_security_origin.h" #include "third_party/blink/public/web/web_console_message.h" #include "third_party/blink/public/web/web_document.h" #include "third_party/blink/public/web/web_document_loader.h" #include "third_party/blink/public/web/web_local_frame.h" #include "third_party/blink/public/web/web_settings.h" #include "third_party/blink/public/web/web_view.h" namespace extensions { namespace { constexpr int kMainWorldId = 0; base::LazyInstance>::DestructorAtExit g_frame_helpers = LAZY_INSTANCE_INITIALIZER; // Returns true if the render frame corresponding with |frame_helper| matches // the given criteria. // // We deliberately do not access any methods that require a v8::Context or // ScriptContext. See also comment below. bool RenderFrameMatches(const ExtensionFrameHelper* frame_helper, mojom::ViewType match_view_type, int match_window_id, int match_tab_id, const std::string& match_extension_id) { if (match_view_type != mojom::ViewType::kInvalid && frame_helper->view_type() != match_view_type) return false; // Not all frames have a valid ViewType, e.g. devtools, most GuestViews, and // unclassified detached WebContents. if (frame_helper->view_type() == mojom::ViewType::kInvalid) return false; // This logic matches ExtensionWebContentsObserver::GetExtensionFromFrame. blink::WebSecurityOrigin origin = frame_helper->render_frame()->GetWebFrame()->GetSecurityOrigin(); if (origin.IsOpaque() || !base::EqualsASCII(origin.Protocol().Utf16(), kExtensionScheme) || !base::EqualsASCII(origin.Host().Utf16(), match_extension_id.c_str())) return false; if (match_window_id != extension_misc::kUnknownWindowId && frame_helper->browser_window_id() != match_window_id) return false; if (match_tab_id != extension_misc::kUnknownTabId && frame_helper->tab_id() != match_tab_id) return false; // Returning handles to frames that haven't created a script context yet // can result in the caller "forcing" a script context (by accessing // properties on the window object). This, in turn, can cause the script // context to be initialized prematurely, with invalid values (e.g., the // inability to retrieve a valid URL from the frame). That then leads to // the ScriptContext being misclassified. // Don't return any frames until they have a valid ScriptContext to limit // the chances for bindings to prematurely initialize these contexts. // This fixes https://crbug.com/1021014. return frame_helper->did_create_script_context(); } // Runs every callback in |callbacks_to_be_run_and_cleared| while |frame_helper| // is valid, and clears |callbacks_to_be_run_and_cleared|. void RunCallbacksWhileFrameIsValid( base::WeakPtr frame_helper, std::vector* callbacks_to_be_run_and_cleared) { // The JavaScript code can cause re-entrancy. To avoid a deadlock, don't run // callbacks that are added during the iteration. std::vector callbacks; callbacks_to_be_run_and_cleared->swap(callbacks); for (auto& callback : callbacks) { std::move(callback).Run(); if (!frame_helper.get()) return; // Frame and ExtensionFrameHelper invalidated by callback. } } enum class PortType { EXTENSION, TAB, NATIVE_APP, }; // Returns an extension hosted in the |render_frame| (or nullptr if the frame // doesn't host an extension). const Extension* GetExtensionFromFrame(content::RenderFrame* render_frame) { DCHECK(render_frame); ScriptContext* context = ScriptContextSet::GetMainWorldContextForFrame(render_frame); return context ? context->effective_extension() : nullptr; } } // namespace ExtensionFrameHelper::ExtensionFrameHelper(content::RenderFrame* render_frame, Dispatcher* extension_dispatcher) : content::RenderFrameObserver(render_frame), content::RenderFrameObserverTracker(render_frame), extension_dispatcher_(extension_dispatcher) { g_frame_helpers.Get().insert(this); if (render_frame->IsMainFrame()) { // Manages its own lifetime. new AutomationApiHelper(render_frame); } render_frame->GetAssociatedInterfaceRegistry()->AddInterface( base::BindRepeating(&ExtensionFrameHelper::BindLocalFrame, weak_ptr_factory_.GetWeakPtr())); } ExtensionFrameHelper::~ExtensionFrameHelper() { g_frame_helpers.Get().erase(this); } // static std::vector ExtensionFrameHelper::GetExtensionFrames( const std::string& extension_id, int browser_window_id, int tab_id, mojom::ViewType view_type) { std::vector render_frames; for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) { if (RenderFrameMatches(helper, view_type, browser_window_id, tab_id, extension_id)) render_frames.push_back(helper->render_frame()); } return render_frames; } // static v8::Local ExtensionFrameHelper::GetV8MainFrames( v8::Local context, const std::string& extension_id, int browser_window_id, int tab_id, mojom::ViewType view_type) { // WebFrame::ScriptCanAccess uses the isolate's current context. We need to // make sure that the current context is the one we're expecting. DCHECK(context == context->GetIsolate()->GetCurrentContext()); std::vector render_frames = GetExtensionFrames(extension_id, browser_window_id, tab_id, view_type); v8::Local v8_frames = v8::Array::New(context->GetIsolate()); int v8_index = 0; for (content::RenderFrame* frame : render_frames) { if (!frame->IsMainFrame()) continue; blink::WebLocalFrame* web_frame = frame->GetWebFrame(); if (!blink::WebFrame::ScriptCanAccess(web_frame)) continue; v8::Local frame_context = web_frame->MainWorldScriptContext(); if (!frame_context.IsEmpty()) { v8::Local window = frame_context->Global(); CHECK(!window.IsEmpty()); v8::Maybe maybe = v8_frames->CreateDataProperty(context, v8_index++, window); CHECK(maybe.IsJust() && maybe.FromJust()); } } return v8_frames; } // static content::RenderFrame* ExtensionFrameHelper::GetBackgroundPageFrame( const std::string& extension_id) { for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) { if (RenderFrameMatches(helper, mojom::ViewType::kExtensionBackgroundPage, extension_misc::kUnknownWindowId, extension_misc::kUnknownTabId, extension_id)) { blink::WebLocalFrame* web_frame = helper->render_frame()->GetWebFrame(); // Check if this is the top frame. if (web_frame->Top() == web_frame) return helper->render_frame(); } } return nullptr; } v8::Local ExtensionFrameHelper::GetV8BackgroundPageMainFrame( v8::Isolate* isolate, const std::string& extension_id) { content::RenderFrame* main_frame = GetBackgroundPageFrame(extension_id); v8::Local background_page; blink::WebLocalFrame* web_frame = main_frame ? main_frame->GetWebFrame() : nullptr; if (web_frame && blink::WebFrame::ScriptCanAccess(web_frame)) background_page = web_frame->MainWorldScriptContext()->Global(); else background_page = v8::Undefined(isolate); return background_page; } // static content::RenderFrame* ExtensionFrameHelper::FindFrame( content::RenderFrame* relative_to_frame, const std::string& name) { // Only pierce browsing instance boundaries if |relative_to_frame| is an // extension. const Extension* extension = GetExtensionFromFrame(relative_to_frame); if (!extension) return nullptr; for (const ExtensionFrameHelper* target : g_frame_helpers.Get()) { // Skip frames with a mismatched name. if (target->render_frame()->GetWebFrame()->AssignedName().Utf8() != name) continue; // Only pierce browsing instance boundaries if the target frame is from the // same extension (but not when another extension shares the same renderer // process because of reuse trigerred by process limit). if (extension != GetExtensionFromFrame(target->render_frame())) continue; return target->render_frame(); } return nullptr; } // static bool ExtensionFrameHelper::IsContextForEventPage(const ScriptContext* context) { content::RenderFrame* render_frame = context->GetRenderFrame(); return context->extension() && render_frame && BackgroundInfo::HasLazyBackgroundPage(context->extension()) && ExtensionFrameHelper::Get(render_frame)->view_type() == mojom::ViewType::kExtensionBackgroundPage; } void ExtensionFrameHelper::BindLocalFrame( mojo::PendingAssociatedReceiver pending_receiver) { local_frame_receiver_.Bind(std::move(pending_receiver)); } void ExtensionFrameHelper::DidCreateDocumentElement() { did_create_current_document_element_ = true; extension_dispatcher_->DidCreateDocumentElement( render_frame()->GetWebFrame()); } void ExtensionFrameHelper::DidCreateNewDocument() { did_create_current_document_element_ = false; } void ExtensionFrameHelper::RunScriptsAtDocumentStart() { DCHECK(did_create_current_document_element_); RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), &document_element_created_callbacks_); // |this| might be dead by now. } void ExtensionFrameHelper::RunScriptsAtDocumentEnd() { RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), &document_load_finished_callbacks_); // |this| might be dead by now. } void ExtensionFrameHelper::RunScriptsAtDocumentIdle() { RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), &document_idle_callbacks_); // |this| might be dead by now. } void ExtensionFrameHelper::ScheduleAtDocumentStart(base::OnceClosure callback) { document_element_created_callbacks_.push_back(std::move(callback)); } void ExtensionFrameHelper::ScheduleAtDocumentEnd(base::OnceClosure callback) { document_load_finished_callbacks_.push_back(std::move(callback)); } void ExtensionFrameHelper::ScheduleAtDocumentIdle(base::OnceClosure callback) { document_idle_callbacks_.push_back(std::move(callback)); } mojom::LocalFrameHost* ExtensionFrameHelper::GetLocalFrameHost() { if (!local_frame_host_remote_.is_bound()) { render_frame()->GetRemoteAssociatedInterfaces()->GetInterface( local_frame_host_remote_.BindNewEndpointAndPassReceiver()); } return local_frame_host_remote_.get(); } void ExtensionFrameHelper::ReadyToCommitNavigation( blink::WebDocumentLoader* document_loader) { // New window created by chrome.app.window.create() must not start parsing the // document immediately. The chrome.app.window.create() callback (if any) // needs to be called prior to the new window's 'load' event. The parser will // be resumed when it happens. It doesn't apply to sandboxed pages. if (view_type_ == mojom::ViewType::kAppWindow && render_frame()->IsMainFrame() && !has_started_first_navigation_ && GURL(document_loader->GetUrl()).SchemeIs(kExtensionScheme) && !ScriptContext::IsSandboxedPage(document_loader->GetUrl())) { document_loader->BlockParser(); } has_started_first_navigation_ = true; if (!delayed_main_world_script_initialization_) return; delayed_main_world_script_initialization_ = false; v8::HandleScope handle_scope(v8::Isolate::GetCurrent()); v8::Local context = render_frame()->GetWebFrame()->MainWorldScriptContext(); v8::Context::Scope context_scope(context); // Normally we would use Document's URL for all kinds of checks, e.g. whether // to inject a content script. However, when committing a navigation, we // should use the URL of a Document being committed instead. This URL is // accessible through WebDocumentLoader::GetURL(). // The scope below temporary maps a frame to a document loader, so that places // which retrieve URL can use the right one. Ideally, we would plumb the // correct URL (or maybe WebDocumentLoader) through the callchain, but there // are many callers which will have to pass nullptr. ScriptContext::ScopedFrameDocumentLoader scoped_document_loader( render_frame()->GetWebFrame(), document_loader); extension_dispatcher_->DidCreateScriptContext(render_frame()->GetWebFrame(), context, kMainWorldId); // TODO(devlin): Add constants for main world id, no extension group. } void ExtensionFrameHelper::DidCommitProvisionalLoad( ui::PageTransition transition) { // Grant cross browsing instance frame lookup if we are an extension. This // should match the conditions in FindFrame. content::RenderFrame* frame = render_frame(); if (GetExtensionFromFrame(frame)) frame->SetAllowsCrossBrowsingInstanceFrameLookup(); } void ExtensionFrameHelper::DidCreateScriptContext( v8::Local context, int32_t world_id) { if (world_id == kMainWorldId) { if (render_frame()->IsBrowserSideNavigationPending()) { // Defer initializing the extensions script context now because it depends // on having the URL of the provisional load which isn't available at this // point. // We can come here twice in the case of window.open(url): first for // about:blank empty document, then possibly for the actual url load // (depends on whoever triggers window proxy init), before getting // ReadyToCommitNavigation. delayed_main_world_script_initialization_ = true; return; } // Sometimes DidCreateScriptContext comes before ReadyToCommitNavigation. // In this case we don't have to wait until ReadyToCommitNavigation. // TODO(dgozman): ensure consistent call order between // DidCreateScriptContext and ReadyToCommitNavigation. delayed_main_world_script_initialization_ = false; } extension_dispatcher_->DidCreateScriptContext(render_frame()->GetWebFrame(), context, world_id); } void ExtensionFrameHelper::WillReleaseScriptContext( v8::Local context, int32_t world_id) { extension_dispatcher_->WillReleaseScriptContext( render_frame()->GetWebFrame(), context, world_id); } bool ExtensionFrameHelper::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(ExtensionFrameHelper, message) IPC_MESSAGE_HANDLER(ExtensionMsg_ValidateMessagePort, OnExtensionValidateMessagePort) IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnConnect, OnExtensionDispatchOnConnect) IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnExtensionDeliverMessage) IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnDisconnect, OnExtensionDispatchOnDisconnect) IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateBrowserWindowId, OnUpdateBrowserWindowId) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } void ExtensionFrameHelper::OnExtensionValidateMessagePort(int worker_thread_id, const PortId& id) { DCHECK_EQ(kMainThreadId, worker_thread_id); extension_dispatcher_->bindings_system() ->messaging_service() ->ValidateMessagePort( extension_dispatcher_->script_context_set_iterator(), id, render_frame()); } void ExtensionFrameHelper::OnExtensionDispatchOnConnect( int worker_thread_id, const PortId& target_port_id, const std::string& channel_name, const ExtensionMsg_TabConnectionInfo& source, const ExtensionMsg_ExternalConnectionInfo& info) { DCHECK_EQ(kMainThreadId, worker_thread_id); extension_dispatcher_->bindings_system() ->messaging_service() ->DispatchOnConnect(extension_dispatcher_->script_context_set_iterator(), target_port_id, channel_name, source, info, render_frame()); } void ExtensionFrameHelper::OnExtensionDeliverMessage(int worker_thread_id, const PortId& target_id, const Message& message) { DCHECK_EQ(kMainThreadId, worker_thread_id); extension_dispatcher_->bindings_system()->messaging_service()->DeliverMessage( extension_dispatcher_->script_context_set_iterator(), target_id, message, render_frame()); } void ExtensionFrameHelper::OnExtensionDispatchOnDisconnect( int worker_thread_id, const PortId& id, const std::string& error_message) { DCHECK_EQ(kMainThreadId, worker_thread_id); extension_dispatcher_->bindings_system() ->messaging_service() ->DispatchOnDisconnect( extension_dispatcher_->script_context_set_iterator(), id, error_message, render_frame()); } void ExtensionFrameHelper::SetTabId(int32_t tab_id) { CHECK_EQ(tab_id_, -1); CHECK_GE(tab_id, 0); tab_id_ = tab_id; } void ExtensionFrameHelper::OnUpdateBrowserWindowId(int browser_window_id) { browser_window_id_ = browser_window_id; } void ExtensionFrameHelper::NotifyRenderViewType(mojom::ViewType type) { // TODO(devlin): It'd be really nice to be able to // DCHECK_EQ(mojom::ViewType::kInvalid, view_type_) here. view_type_ = type; } void ExtensionFrameHelper::MessageInvoke(const std::string& extension_id, const std::string& module_name, const std::string& function_name, const base::Value args) { extension_dispatcher_->InvokeModuleSystemMethod( render_frame(), extension_id, module_name, function_name, base::Value::AsListValue(args)); } void ExtensionFrameHelper::ExecuteCode(mojom::ExecuteCodeParamsPtr param, ExecuteCodeCallback callback) { // Sanity checks. if (param->injection->is_css()) { if (param->injection->get_css()->sources.empty()) { mojo::ReportBadMessage("At least one CSS source must be specified."); return; } if (param->injection->get_css()->operation == mojom::CSSInjection::Operation::kRemove && !base::ranges::all_of(param->injection->get_css()->sources, [](const mojom::CSSSourcePtr& source) { return source->key.has_value(); })) { mojo::ReportBadMessage( "An injection key must be specified for CSS removal."); return; } } else { DCHECK(param->injection->is_js()); // Enforced by mojo. if (param->injection->get_js()->sources.empty()) { mojo::ReportBadMessage("At least one JS source must be specified."); return; } } extension_dispatcher_->ExecuteCode(std::move(param), std::move(callback), render_frame()); } void ExtensionFrameHelper::SetFrameName(const std::string& name) { render_frame()->GetWebFrame()->SetName(blink::WebString::FromUTF8(name)); } void ExtensionFrameHelper::AppWindowClosed(bool send_onclosed) { DCHECK(render_frame()->IsMainFrame()); if (!send_onclosed) return; v8::HandleScope scope(v8::Isolate::GetCurrent()); v8::Local v8_context = render_frame()->GetWebFrame()->MainWorldScriptContext(); ScriptContext* script_context = ScriptContextSet::GetContextByV8Context(v8_context); if (!script_context) return; script_context->module_system()->CallModuleMethodSafe("app.window", "onAppWindowClosed"); } void ExtensionFrameHelper::SetSpatialNavigationEnabled(bool enabled) { render_frame() ->GetWebView() ->GetSettings() ->SetSpatialNavigationEnabled(enabled); } void ExtensionFrameHelper::ExecuteDeclarativeScript( int32_t tab_id, const std::string& extension_id, const std::string& script_id, const GURL& url) { // TODO(https://crbug.com/1186220): URL-checking isn't the best approach to // avoid user data leak. Consider what we can do to mitigate this case. // Begin script injection workflow only if the current URL is identical to the // one that matched declarative conditions in the browser. if (GURL(render_frame()->GetWebFrame()->GetDocument().Url()) == url) { extension_dispatcher_->ExecuteDeclarativeScript( render_frame(), tab_id, extension_id, script_id, url); } } void ExtensionFrameHelper::OnDestruct() { delete this; } void ExtensionFrameHelper::DraggableRegionsChanged() { if (!render_frame()->IsMainFrame()) return; blink::WebVector webregions = render_frame()->GetWebFrame()->GetDocument().DraggableRegions(); std::vector regions; for (blink::WebDraggableRegion& webregion : webregions) { render_frame()->ConvertViewportToWindow(&webregion.bounds); regions.push_back(DraggableRegion()); DraggableRegion& region = regions.back(); region.bounds = webregion.bounds; region.draggable = webregion.draggable; } Send(new ExtensionHostMsg_UpdateDraggableRegions(routing_id(), regions)); } } // namespace extensions