// Copyright (c) 2019 Slack Technologies, Inc. // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. #include "shell/renderer/api/electron_api_context_bridge.h" #include #include #include #include #include #include "base/feature_list.h" #include "base/no_destructor.h" #include "base/strings/string_number_conversions.h" #include "base/trace_event/trace_event.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame_observer.h" #include "shell/common/api/object_life_monitor.h" #include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/callback_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/promise.h" #include "shell/common/node_includes.h" #include "shell/common/world_ids.h" #include "third_party/blink/public/web/web_blob.h" #include "third_party/blink/public/web/web_element.h" #include "third_party/blink/public/web/web_local_frame.h" namespace features { const base::Feature kContextBridgeMutability{"ContextBridgeMutability", base::FEATURE_DISABLED_BY_DEFAULT}; } namespace electron { content::RenderFrame* GetRenderFrame(v8::Local value); namespace api { namespace context_bridge { const char* const kProxyFunctionPrivateKey = "electron_contextBridge_proxy_fn"; const char* const kSupportsDynamicPropertiesPrivateKey = "electron_contextBridge_supportsDynamicProperties"; const char* const kOriginalFunctionPrivateKey = "electron_contextBridge_original_fn"; } // namespace context_bridge namespace { static int kMaxRecursion = 1000; // Returns true if |maybe| is both a value, and that value is true. inline bool IsTrue(v8::Maybe maybe) { return maybe.IsJust() && maybe.FromJust(); } // Sourced from "extensions/renderer/v8_schema_registry.cc" // Recursively freezes every v8 object on |object|. bool DeepFreeze(const v8::Local& object, const v8::Local& context, std::set frozen = std::set()) { int hash = object->GetIdentityHash(); if (frozen.find(hash) != frozen.end()) return true; frozen.insert(hash); v8::Local property_names = object->GetOwnPropertyNames(context).ToLocalChecked(); for (uint32_t i = 0; i < property_names->Length(); ++i) { v8::Local child = object->Get(context, property_names->Get(context, i).ToLocalChecked()) .ToLocalChecked(); if (child->IsObject() && !child->IsTypedArray()) { if (!DeepFreeze(child.As(), context, frozen)) return false; } } return IsTrue( object->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen)); } bool IsPlainObject(const v8::Local& object) { if (!object->IsObject()) return false; return !(object->IsNullOrUndefined() || object->IsDate() || object->IsArgumentsObject() || object->IsBigIntObject() || object->IsBooleanObject() || object->IsNumberObject() || object->IsStringObject() || object->IsSymbolObject() || object->IsNativeError() || object->IsRegExp() || object->IsPromise() || object->IsMap() || object->IsSet() || object->IsMapIterator() || object->IsSetIterator() || object->IsWeakMap() || object->IsWeakSet() || object->IsArrayBuffer() || object->IsArrayBufferView() || object->IsArray() || object->IsDataView() || object->IsSharedArrayBuffer() || object->IsProxy() || object->IsWasmModuleObject() || object->IsWasmMemoryObject() || object->IsModuleNamespaceObject()); } bool IsPlainArray(const v8::Local& arr) { if (!arr->IsArray()) return false; return !arr->IsTypedArray(); } void SetPrivate(v8::Local context, v8::Local target, const std::string& key, v8::Local value) { target ->SetPrivate( context, v8::Private::ForApi(context->GetIsolate(), gin::StringToV8(context->GetIsolate(), key)), value) .Check(); } v8::MaybeLocal GetPrivate(v8::Local context, v8::Local target, const std::string& key) { return target->GetPrivate( context, v8::Private::ForApi(context->GetIsolate(), gin::StringToV8(context->GetIsolate(), key))); } } // namespace v8::MaybeLocal PassValueToOtherContext( v8::Local source_context, v8::Local destination_context, v8::Local value, context_bridge::ObjectCache* object_cache, bool support_dynamic_properties, int recursion_depth, BridgeErrorTarget error_target) { TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext"); if (recursion_depth >= kMaxRecursion) { v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource ? source_context : destination_context); source_context->GetIsolate()->ThrowException(v8::Exception::TypeError( gin::StringToV8(source_context->GetIsolate(), "Electron contextBridge recursion depth exceeded. " "Nested objects " "deeper than 1000 are not supported."))); return v8::MaybeLocal(); } // Certain primitives always use the current contexts prototype and we can // pass these through directly which is significantly more performant than // copying them. This list of primitives is based on the classification of // "primitive value" as defined in the ECMA262 spec // https://tc39.es/ecma262/#sec-primitive-value if (value->IsString() || value->IsNumber() || value->IsNullOrUndefined() || value->IsBoolean() || value->IsSymbol() || value->IsBigInt()) { return v8::MaybeLocal(value); } // Check Cache auto cached_value = object_cache->GetCachedProxiedObject(value); if (!cached_value.IsEmpty()) { return cached_value; } // Proxy functions and monitor the lifetime in the new context to release // the global handle at the right time. if (value->IsFunction()) { auto func = value.As(); v8::MaybeLocal maybe_original_fn = GetPrivate( source_context, func, context_bridge::kOriginalFunctionPrivateKey); { v8::Context::Scope destination_scope(destination_context); v8::Local proxy_func; // If this function has already been sent over the bridge, // then it is being sent _back_ over the bridge and we can // simply return the original method here for performance reasons // For safety reasons we check if the destination context is the // creation context of the original method. If it's not we proceed // with the proxy logic if (maybe_original_fn.ToLocal(&proxy_func) && proxy_func->IsFunction() && proxy_func.As()->CreationContext() == destination_context) { return v8::MaybeLocal(proxy_func); } v8::Local state = v8::Object::New(destination_context->GetIsolate()); SetPrivate(destination_context, state, context_bridge::kProxyFunctionPrivateKey, func); SetPrivate(destination_context, state, context_bridge::kSupportsDynamicPropertiesPrivateKey, gin::ConvertToV8(destination_context->GetIsolate(), support_dynamic_properties)); if (!v8::Function::New(destination_context, ProxyFunctionWrapper, state) .ToLocal(&proxy_func)) return v8::MaybeLocal(); SetPrivate(destination_context, proxy_func.As(), context_bridge::kOriginalFunctionPrivateKey, func); object_cache->CacheProxiedObject(value, proxy_func); return v8::MaybeLocal(proxy_func); } } // Proxy promises as they have a safe and guaranteed memory lifecycle if (value->IsPromise()) { v8::Context::Scope destination_scope(destination_context); auto source_promise = value.As(); // Make the promise a shared_ptr so that when the original promise is // freed the proxy promise is correctly freed as well instead of being // left dangling std::shared_ptr>> proxied_promise( new gin_helper::Promise>( destination_context->GetIsolate())); v8::Local proxied_promise_handle = proxied_promise->GetHandle(); v8::Global global_then_source_context( source_context->GetIsolate(), source_context); v8::Global global_then_destination_context( destination_context->GetIsolate(), destination_context); global_then_source_context.SetWeak(); global_then_destination_context.SetWeak(); auto then_cb = base::BindOnce( [](std::shared_ptr>> proxied_promise, v8::Isolate* isolate, v8::Global global_source_context, v8::Global global_destination_context, v8::Local result) { if (global_source_context.IsEmpty() || global_destination_context.IsEmpty()) return; context_bridge::ObjectCache object_cache; auto val = PassValueToOtherContext(global_source_context.Get(isolate), global_destination_context.Get(isolate), result, &object_cache, false, 0); if (!val.IsEmpty()) proxied_promise->Resolve(val.ToLocalChecked()); }, proxied_promise, destination_context->GetIsolate(), std::move(global_then_source_context), std::move(global_then_destination_context)); v8::Global global_catch_source_context( source_context->GetIsolate(), source_context); v8::Global global_catch_destination_context( destination_context->GetIsolate(), destination_context); global_catch_source_context.SetWeak(); global_catch_destination_context.SetWeak(); auto catch_cb = base::BindOnce( [](std::shared_ptr>> proxied_promise, v8::Isolate* isolate, v8::Global global_source_context, v8::Global global_destination_context, v8::Local result) { if (global_source_context.IsEmpty() || global_destination_context.IsEmpty()) return; context_bridge::ObjectCache object_cache; auto val = PassValueToOtherContext(global_source_context.Get(isolate), global_destination_context.Get(isolate), result, &object_cache, false, 0); if (!val.IsEmpty()) proxied_promise->Reject(val.ToLocalChecked()); }, proxied_promise, destination_context->GetIsolate(), std::move(global_catch_source_context), std::move(global_catch_destination_context)); ignore_result(source_promise->Then( source_context, gin::ConvertToV8(destination_context->GetIsolate(), then_cb) .As(), gin::ConvertToV8(destination_context->GetIsolate(), catch_cb) .As())); object_cache->CacheProxiedObject(value, proxied_promise_handle); return v8::MaybeLocal(proxied_promise_handle); } // Errors aren't serializable currently, we need to pull the message out and // re-construct in the destination context if (value->IsNativeError()) { v8::Context::Scope destination_context_scope(destination_context); // We should try to pull "message" straight off of the error as a // v8::Message includes some pretext that can get duplicated each time it // crosses the bridge we fallback to the v8::Message approach if we can't // pull "message" for some reason v8::MaybeLocal maybe_message = value.As()->Get( source_context, gin::ConvertToV8(source_context->GetIsolate(), "message")); v8::Local message; if (maybe_message.ToLocal(&message) && message->IsString()) { return v8::MaybeLocal( v8::Exception::Error(message.As())); } return v8::MaybeLocal(v8::Exception::Error( v8::Exception::CreateMessage(destination_context->GetIsolate(), value) ->Get())); } // Manually go through the array and pass each value individually into a new // array so that functions deep inside arrays get proxied or arrays of // promises are proxied correctly. if (IsPlainArray(value)) { v8::Context::Scope destination_context_scope(destination_context); v8::Local arr = value.As(); size_t length = arr->Length(); v8::Local cloned_arr = v8::Array::New(destination_context->GetIsolate(), length); for (size_t i = 0; i < length; i++) { auto value_for_array = PassValueToOtherContext( source_context, destination_context, arr->Get(source_context, i).ToLocalChecked(), object_cache, support_dynamic_properties, recursion_depth + 1); if (value_for_array.IsEmpty()) return v8::MaybeLocal(); if (!IsTrue(cloned_arr->Set(destination_context, static_cast(i), value_for_array.ToLocalChecked()))) { return v8::MaybeLocal(); } } object_cache->CacheProxiedObject(value, cloned_arr); return v8::MaybeLocal(cloned_arr); } // Custom logic to "clone" Element references blink::WebElement elem = blink::WebElement::FromV8Value(value); if (!elem.IsNull()) { v8::Context::Scope destination_context_scope(destination_context); return v8::MaybeLocal(elem.ToV8Value( destination_context->Global(), destination_context->GetIsolate())); } // Custom logic to "clone" Blob references blink::WebBlob blob = blink::WebBlob::FromV8Value(value); if (!blob.IsNull()) { v8::Context::Scope destination_context_scope(destination_context); return v8::MaybeLocal(blob.ToV8Value( destination_context->Global(), destination_context->GetIsolate())); } // Proxy all objects if (IsPlainObject(value)) { auto object_value = value.As(); auto passed_value = CreateProxyForAPI( object_value, source_context, destination_context, object_cache, support_dynamic_properties, recursion_depth + 1); if (passed_value.IsEmpty()) return v8::MaybeLocal(); return v8::MaybeLocal(passed_value.ToLocalChecked()); } // Serializable objects blink::CloneableMessage ret; { v8::Local error_context = error_target == BridgeErrorTarget::kSource ? source_context : destination_context; v8::Context::Scope error_scope(error_context); // V8 serializer will throw an error if required if (!gin::ConvertFromV8(error_context->GetIsolate(), value, &ret)) return v8::MaybeLocal(); } { v8::Context::Scope destination_context_scope(destination_context); v8::Local cloned_value = gin::ConvertToV8(destination_context->GetIsolate(), ret); object_cache->CacheProxiedObject(value, cloned_value); return v8::MaybeLocal(cloned_value); } } void ProxyFunctionWrapper(const v8::FunctionCallbackInfo& info) { TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper"); CHECK(info.Data()->IsObject()); v8::Local data = info.Data().As(); bool support_dynamic_properties = false; gin::Arguments args(info); // Context the proxy function was called from v8::Local calling_context = args.isolate()->GetCurrentContext(); // Pull the original function and its context off of the data private key v8::MaybeLocal sdp_value = GetPrivate(calling_context, data, context_bridge::kSupportsDynamicPropertiesPrivateKey); v8::MaybeLocal maybe_func = GetPrivate( calling_context, data, context_bridge::kProxyFunctionPrivateKey); v8::Local func_value; if (sdp_value.IsEmpty() || maybe_func.IsEmpty() || !gin::ConvertFromV8(args.isolate(), sdp_value.ToLocalChecked(), &support_dynamic_properties) || !maybe_func.ToLocal(&func_value)) return; v8::Local func = func_value.As(); v8::Local func_owning_context = func->CreationContext(); { v8::Context::Scope func_owning_context_scope(func_owning_context); context_bridge::ObjectCache object_cache; std::vector> original_args; std::vector> proxied_args; args.GetRemaining(&original_args); for (auto value : original_args) { auto arg = PassValueToOtherContext(calling_context, func_owning_context, value, &object_cache, support_dynamic_properties, 0); if (arg.IsEmpty()) return; proxied_args.push_back(arg.ToLocalChecked()); } v8::MaybeLocal maybe_return_value; bool did_error = false; v8::Local error_message; { v8::TryCatch try_catch(args.isolate()); maybe_return_value = func->Call(func_owning_context, func, proxied_args.size(), proxied_args.data()); if (try_catch.HasCaught()) { did_error = true; v8::Local exception = try_catch.Exception(); const char* err_msg = "An unknown exception occurred in the isolated context, an error " "occurred but a valid exception was not thrown."; if (!exception->IsNull() && exception->IsObject()) { v8::MaybeLocal maybe_message = exception.As()->Get( func_owning_context, gin::ConvertToV8(args.isolate(), "message")); if (!maybe_message.ToLocal(&error_message) || !error_message->IsString()) { error_message = gin::StringToV8(args.isolate(), err_msg); } } else { error_message = gin::StringToV8(args.isolate(), err_msg); } } } if (did_error) { v8::Context::Scope calling_context_scope(calling_context); args.isolate()->ThrowException( v8::Exception::Error(error_message.As())); return; } if (maybe_return_value.IsEmpty()) return; auto ret = PassValueToOtherContext( func_owning_context, calling_context, maybe_return_value.ToLocalChecked(), &object_cache, support_dynamic_properties, 0, BridgeErrorTarget::kDestination); if (ret.IsEmpty()) return; info.GetReturnValue().Set(ret.ToLocalChecked()); } } v8::MaybeLocal CreateProxyForAPI( const v8::Local& api_object, const v8::Local& source_context, const v8::Local& destination_context, context_bridge::ObjectCache* object_cache, bool support_dynamic_properties, int recursion_depth) { gin_helper::Dictionary api(source_context->GetIsolate(), api_object); { v8::Context::Scope destination_context_scope(destination_context); gin_helper::Dictionary proxy = gin::Dictionary::CreateEmpty(destination_context->GetIsolate()); object_cache->CacheProxiedObject(api.GetHandle(), proxy.GetHandle()); auto maybe_keys = api.GetHandle()->GetOwnPropertyNames( source_context, static_cast(v8::ONLY_ENUMERABLE)); if (maybe_keys.IsEmpty()) return v8::MaybeLocal(proxy.GetHandle()); auto keys = maybe_keys.ToLocalChecked(); uint32_t length = keys->Length(); for (uint32_t i = 0; i < length; i++) { v8::Local key = keys->Get(destination_context, i).ToLocalChecked(); if (support_dynamic_properties) { v8::Context::Scope source_context_scope(source_context); auto maybe_desc = api.GetHandle()->GetOwnPropertyDescriptor( source_context, key.As()); v8::Local desc_value; if (!maybe_desc.ToLocal(&desc_value) || !desc_value->IsObject()) continue; gin_helper::Dictionary desc(api.isolate(), desc_value.As()); if (desc.Has("get") || desc.Has("set")) { v8::Local getter; v8::Local setter; desc.Get("get", &getter); desc.Get("set", &setter); { v8::Context::Scope destination_context_scope(destination_context); v8::Local getter_proxy; v8::Local setter_proxy; if (!getter.IsEmpty()) { if (!PassValueToOtherContext(source_context, destination_context, getter, object_cache, support_dynamic_properties, 1) .ToLocal(&getter_proxy)) continue; } if (!setter.IsEmpty()) { if (!PassValueToOtherContext(source_context, destination_context, setter, object_cache, support_dynamic_properties, 1) .ToLocal(&setter_proxy)) continue; } v8::PropertyDescriptor desc(getter_proxy, setter_proxy); ignore_result(proxy.GetHandle()->DefineProperty( destination_context, key.As(), desc)); } continue; } } v8::Local value; if (!api.Get(key, &value)) continue; auto passed_value = PassValueToOtherContext( source_context, destination_context, value, object_cache, support_dynamic_properties, recursion_depth + 1); if (passed_value.IsEmpty()) return v8::MaybeLocal(); proxy.Set(key, passed_value.ToLocalChecked()); } return proxy.GetHandle(); } } void ExposeAPIInMainWorld(v8::Isolate* isolate, const std::string& key, v8::Local api, gin_helper::Arguments* args) { TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key); auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); v8::Local main_context = frame->MainWorldScriptContext(); gin_helper::Dictionary global(main_context->GetIsolate(), main_context->Global()); if (global.Has(key)) { args->ThrowError( "Cannot bind an API on top of an existing property on the window " "object"); return; } v8::Local isolated_context = frame->WorldScriptContext(args->isolate(), WorldIDs::ISOLATED_WORLD_ID); { context_bridge::ObjectCache object_cache; v8::Context::Scope main_context_scope(main_context); v8::MaybeLocal maybe_proxy = PassValueToOtherContext( isolated_context, main_context, api, &object_cache, false, 0); if (maybe_proxy.IsEmpty()) return; auto proxy = maybe_proxy.ToLocalChecked(); if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) { global.Set(key, proxy); return; } if (proxy->IsObject() && !proxy->IsTypedArray() && !DeepFreeze(proxy.As(), main_context)) return; global.SetReadOnlyNonConfigurable(key, proxy); } } gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start, const std::vector& key_path) { gin_helper::Dictionary current = start; for (size_t i = 0; i < key_path.size() - 1; i++) { CHECK(current.Get(key_path[i], ¤t)); } return current; } void OverrideGlobalValueFromIsolatedWorld( const std::vector& key_path, v8::Local value, bool support_dynamic_properties) { if (key_path.empty()) return; auto* render_frame = GetRenderFrame(value); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); v8::Local main_context = frame->MainWorldScriptContext(); gin_helper::Dictionary global(main_context->GetIsolate(), main_context->Global()); const std::string final_key = key_path[key_path.size() - 1]; gin_helper::Dictionary target_object = TraceKeyPath(global, key_path); { v8::Context::Scope main_context_scope(main_context); context_bridge::ObjectCache object_cache; v8::MaybeLocal maybe_proxy = PassValueToOtherContext(value->CreationContext(), main_context, value, &object_cache, support_dynamic_properties, 1); DCHECK(!maybe_proxy.IsEmpty()); auto proxy = maybe_proxy.ToLocalChecked(); target_object.Set(final_key, proxy); } } bool OverrideGlobalPropertyFromIsolatedWorld( const std::vector& key_path, v8::Local getter, v8::Local setter, gin_helper::Arguments* args) { if (key_path.empty()) return false; auto* render_frame = GetRenderFrame(getter); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); v8::Local main_context = frame->MainWorldScriptContext(); gin_helper::Dictionary global(main_context->GetIsolate(), main_context->Global()); const std::string final_key = key_path[key_path.size() - 1]; v8::Local target_object = TraceKeyPath(global, key_path).GetHandle(); { v8::Context::Scope main_context_scope(main_context); context_bridge::ObjectCache object_cache; v8::Local getter_proxy; v8::Local setter_proxy; if (!getter->IsNullOrUndefined()) { v8::MaybeLocal maybe_getter_proxy = PassValueToOtherContext(getter->CreationContext(), main_context, getter, &object_cache, false, 1); DCHECK(!maybe_getter_proxy.IsEmpty()); getter_proxy = maybe_getter_proxy.ToLocalChecked(); } if (!setter->IsNullOrUndefined() && setter->IsObject()) { v8::MaybeLocal maybe_setter_proxy = PassValueToOtherContext(getter->CreationContext(), main_context, setter, &object_cache, false, 1); DCHECK(!maybe_setter_proxy.IsEmpty()); setter_proxy = maybe_setter_proxy.ToLocalChecked(); } v8::PropertyDescriptor desc(getter_proxy, setter_proxy); bool success = IsTrue(target_object->DefineProperty( main_context, gin::StringToV8(args->isolate(), final_key), desc)); DCHECK(success); return success; } } bool IsCalledFromMainWorld(v8::Isolate* isolate) { auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); v8::Local main_context = frame->MainWorldScriptContext(); return isolate->GetCurrentContext() == main_context; } } // namespace api } // namespace electron namespace { void Initialize(v8::Local exports, v8::Local unused, v8::Local context, void* priv) { v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld); dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", &electron::api::OverrideGlobalValueFromIsolatedWorld); dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", &electron::api::OverrideGlobalPropertyFromIsolatedWorld); dict.SetMethod("_isCalledFromMainWorld", &electron::api::IsCalledFromMainWorld); #ifdef DCHECK_IS_ON dict.Set("_isDebug", true); #endif } } // namespace NODE_LINKED_MODULE_CONTEXT_AWARE(electron_renderer_context_bridge, Initialize)