| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include "components/js_injection/renderer/js_binding.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/strings/string_util.h" |
| #include "components/js_injection/common/interfaces.mojom-forward.h" |
| #include "components/js_injection/renderer/js_communication.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "gin/converter.h" |
| #include "gin/data_object_builder.h" |
| #include "gin/object_template_builder.h" |
| #include "third_party/abseil-cpp/absl/functional/overload.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/messaging/message_port_channel.h" |
| #include "third_party/blink/public/common/messaging/string_message_codec.h" |
| #include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h" |
| #include "third_party/blink/public/platform/web_security_origin.h" |
| #include "third_party/blink/public/web/web_frame.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/public/web/web_message_port_converter.h" |
| #include "v8/include/cppgc/allocation.h" |
| #include "v8/include/v8-cppgc.h" |
| #include "v8/include/v8.h" |
| |
| namespace { |
| |
| constexpr char kPostMessage[] = "postMessage"; |
| constexpr char kOnMessage[] = "onmessage"; |
| constexpr char kAddEventListener[] = "addEventListener"; |
| constexpr char kRemoveEventListener[] = "removeEventListener"; |
| |
| class V8ArrayBufferPayload : public blink::WebMessageArrayBufferPayload { |
| public: |
| explicit V8ArrayBufferPayload(v8::Local<v8::ArrayBuffer> array_buffer) |
| : array_buffer_(array_buffer) { |
| CHECK(!array_buffer_.IsEmpty()); |
| } |
| |
| // Although resize *may* be supported, it's not needed to be handled for JS to |
| // browser messaging. |
| bool GetIsResizableByUserJavaScript() const override { return false; } |
| |
| size_t GetMaxByteLength() const override { return GetLength(); } |
| |
| size_t GetLength() const override { return array_buffer_->ByteLength(); } |
| |
| std::optional<base::span<const uint8_t>> GetAsSpanIfPossible() |
| const override { |
| return base::span(static_cast<const uint8_t*>(array_buffer_->Data()), |
| array_buffer_->ByteLength()); |
| } |
| |
| void CopyInto(base::span<uint8_t> dest) const override { |
| CHECK_GE(dest.size(), array_buffer_->ByteLength()); |
| memcpy(dest.data(), array_buffer_->Data(), array_buffer_->ByteLength()); |
| } |
| |
| private: |
| v8::Local<v8::ArrayBuffer> array_buffer_; |
| }; |
| |
| } // namespace |
| |
| namespace js_injection { |
| |
| // static |
| cppgc::WeakPersistent<JsBinding> JsBinding::Install( |
| content::RenderFrame* render_frame, |
| const std::u16string& js_object_name, |
| base::WeakPtr<JsCommunication> js_communication, |
| v8::Isolate* isolate, |
| v8::Local<v8::Context> context) { |
| CHECK(!js_object_name.empty()) |
| << "JavaScript wrapper name shouldn't be empty"; |
| |
| std::optional<v8::HandleScope> handle_scope; |
| std::optional<v8::Context::Scope> context_scope; |
| // The scopes may have already been setup outside this method. |
| if (!isolate) { |
| blink::WebLocalFrame* web_frame = render_frame->GetWebFrame(); |
| isolate = web_frame->GetAgentGroupScheduler()->Isolate(); |
| handle_scope.emplace(isolate); |
| context = web_frame->MainWorldScriptContext(); |
| if (context.IsEmpty()) { |
| return nullptr; |
| } |
| |
| context_scope.emplace(context); |
| } |
| JsBinding* js_binding = cppgc::MakeGarbageCollected<JsBinding>( |
| isolate->GetCppHeap()->GetAllocationHandle(), render_frame, |
| js_object_name, js_communication); |
| v8::Local<v8::Object> wrapper; |
| if (!js_binding->GetWrapper(isolate).ToLocal(&wrapper)) { |
| return nullptr; |
| } |
| |
| v8::Local<v8::Object> global = context->Global(); |
| global |
| ->CreateDataProperty( |
| context, gin::StringToSymbol(isolate, js_object_name), wrapper) |
| .Check(); |
| |
| return js_binding; |
| } |
| |
| JsBinding::JsBinding(content::RenderFrame* render_frame, |
| const std::u16string& js_object_name, |
| base::WeakPtr<JsCommunication> js_communication) |
| : render_frame_(render_frame), |
| js_object_name_(js_object_name), |
| js_communication_(js_communication) { |
| } |
| |
| JsBinding::~JsBinding() = default; |
| |
| void JsBinding::OnPostMessage(blink::WebMessagePayload message) { |
| // If `js_communication_` is null, this object will soon be destroyed. |
| if (!js_communication_) |
| return; |
| |
| blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); |
| if (!web_frame) |
| return; |
| v8::Isolate* isolate = web_frame->GetAgentGroupScheduler()->Isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| v8::Local<v8::Context> context = web_frame->MainWorldScriptContext(); |
| if (context.IsEmpty()) |
| return; |
| |
| v8::Context::Scope context_scope(context); |
| // Setting verbose makes the exception get reported to the default |
| // uncaught-exception handlers, rather than just being silently swallowed. |
| v8::TryCatch try_catch(isolate); |
| try_catch.SetVerbose(true); |
| |
| v8::Local<v8::Value> v8_message = std::visit( |
| absl::Overload{ |
| [isolate](std::u16string& string_value) -> v8::Local<v8::Value> { |
| return gin::ConvertToV8(isolate, std::move(string_value)); |
| }, |
| [isolate](std::unique_ptr<blink::WebMessageArrayBufferPayload>& |
| array_buffer_value) -> v8::Local<v8::Value> { |
| auto backing_store = v8::ArrayBuffer::NewBackingStore( |
| isolate, array_buffer_value->GetLength()); |
| CHECK(backing_store->ByteLength() == |
| array_buffer_value->GetLength()); |
| array_buffer_value->CopyInto( |
| base::span(static_cast<uint8_t*>(backing_store->Data()), |
| backing_store->ByteLength())); |
| return v8::ArrayBuffer::New(isolate, std::move(backing_store)); |
| }}, |
| message); |
| |
| // Simulate MessageEvent's data property. See |
| // https://html.spec.whatwg.org/multipage/comms.html#messageevent |
| v8::Local<v8::Object> event = |
| gin::DataObjectBuilder(isolate).Set("data", v8_message).Build(); |
| v8::Local<v8::Value> argv[] = {event}; |
| |
| v8::Local<v8::Object> self = GetWrapper(isolate).ToLocalChecked(); |
| v8::Local<v8::Function> on_message = GetOnMessage(isolate); |
| if (!on_message.IsEmpty()) { |
| web_frame->RequestExecuteV8Function(context, on_message, self, 1, argv, {}); |
| } |
| |
| // Copy the listeners so that if the listener modifies the list in some way |
| // there isn't a UAF. |
| v8::LocalVector<v8::Function> listeners_copy(isolate); |
| listeners_copy.reserve(listeners_.size()); |
| for (const auto& listener : listeners_) { |
| listeners_copy.push_back(listener.Get(isolate)); |
| } |
| for (const auto& listener : listeners_copy) { |
| // Ensure the listener is still registered. |
| if (find_listener(listener) != listeners_.end()) { |
| web_frame->RequestExecuteV8Function(context, listener, self, 1, argv, {}); |
| } |
| } |
| } |
| |
| void JsBinding::ReleaseV8GlobalObjects() { |
| listeners_.clear(); |
| on_message_.Reset(); |
| } |
| |
| void JsBinding::Bind( |
| mojo::PendingAssociatedReceiver<mojom::BrowserToJsMessaging> receiver) { |
| receiver_.reset(); |
| return receiver_.Bind(std::move(receiver)); |
| } |
| |
| gin::ObjectTemplateBuilder JsBinding::GetObjectTemplateBuilder( |
| v8::Isolate* isolate) { |
| return gin::Wrappable<JsBinding>::GetObjectTemplateBuilder(isolate) |
| .SetMethod(kPostMessage, &JsBinding::PostMessage) |
| .SetMethod(kAddEventListener, &JsBinding::AddEventListener) |
| .SetMethod(kRemoveEventListener, &JsBinding::RemoveEventListener) |
| .SetProperty(kOnMessage, &JsBinding::GetOnMessage, |
| &JsBinding::SetOnMessage); |
| } |
| |
| void JsBinding::PostMessage(gin::Arguments* args) { |
| v8::Local<v8::Value> js_payload; |
| if (!args->GetNext(&js_payload)) { |
| args->ThrowError(); |
| return; |
| } |
| blink::WebMessagePayload message_payload; |
| if (js_payload->IsString()) { |
| std::u16string string; |
| gin::Converter<std::u16string>::FromV8(args->isolate(), js_payload, |
| &string); |
| message_payload = std::move(string); |
| } else if (js_payload->IsArrayBuffer()) { |
| v8::Local<v8::ArrayBuffer> array_buffer = js_payload.As<v8::ArrayBuffer>(); |
| message_payload = std::make_unique<V8ArrayBufferPayload>(array_buffer); |
| } else { |
| args->ThrowError(); |
| return; |
| } |
| |
| std::vector<blink::MessagePortChannel> ports; |
| v8::LocalVector<v8::Object> objs(args->isolate()); |
| // If we get more than two arguments and the second argument is not an array |
| // of ports, we can't process. |
| if (args->Length() >= 2 && !args->GetNext(&objs)) { |
| args->ThrowError(); |
| return; |
| } |
| |
| for (auto& obj : objs) { |
| std::optional<blink::MessagePortChannel> port = |
| blink::WebMessagePortConverter::DisentangleAndExtractMessagePortChannel( |
| args->isolate(), obj); |
| // If the port is null we should throw an exception. |
| if (!port.has_value()) { |
| args->ThrowError(); |
| return; |
| } |
| ports.emplace_back(port.value()); |
| } |
| |
| mojom::JsToBrowserMessaging* js_to_java_messaging = |
| js_communication_ ? js_communication_->GetJsToJavaMessage(js_object_name_) |
| : nullptr; |
| if (js_to_java_messaging) { |
| js_to_java_messaging->PostMessage( |
| std::move(message_payload), |
| blink::MessagePortChannel::ReleaseHandles(ports)); |
| } |
| } |
| |
| // AddEventListener() needs to match EventTarget's AddEventListener() in blink. |
| // It takes |type|, |listener| parameters, we ignore the |options| parameter. |
| // See https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener |
| void JsBinding::AddEventListener(gin::Arguments* args) { |
| std::string type; |
| if (!args->GetNext(&type)) { |
| args->ThrowError(); |
| return; |
| } |
| |
| // We only support message event. |
| if (type != "message") |
| return; |
| |
| v8::Local<v8::Function> listener; |
| if (!args->GetNext(&listener)) |
| return; |
| |
| // Should be at most 3 parameters. |
| if (args->Length() > 3) { |
| args->ThrowError(); |
| return; |
| } |
| |
| if (find_listener(listener) != listeners_.end()) { |
| return; |
| } |
| |
| v8::Local<v8::Context> context = args->GetHolderCreationContext(); |
| listeners_.push_back( |
| v8::Global<v8::Function>(context->GetIsolate(), listener)); |
| } |
| |
| // RemoveEventListener() needs to match EventTarget's RemoveEventListener() in |
| // blink. It takes |type|, |listener| parameters, we ignore |options| parameter. |
| // See https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener |
| void JsBinding::RemoveEventListener(gin::Arguments* args) { |
| std::string type; |
| if (!args->GetNext(&type)) { |
| args->ThrowError(); |
| return; |
| } |
| |
| // We only support message event. |
| if (type != "message") |
| return; |
| |
| v8::Local<v8::Function> listener; |
| if (!args->GetNext(&listener)) |
| return; |
| |
| // Should be at most 3 parameters. |
| if (args->Length() > 3) { |
| args->ThrowError(); |
| return; |
| } |
| |
| if (auto iter = find_listener(listener); iter != listeners_.end()) { |
| listeners_.erase(iter); |
| } |
| } |
| |
| v8::Local<v8::Function> JsBinding::GetOnMessage(v8::Isolate* isolate) { |
| return on_message_.Get(isolate); |
| } |
| |
| void JsBinding::SetOnMessage(v8::Isolate* isolate, v8::Local<v8::Value> value) { |
| if (value->IsFunction()) |
| on_message_.Reset(isolate, value.As<v8::Function>()); |
| else |
| on_message_.Reset(); |
| } |
| |
| const gin::WrapperInfo* JsBinding::wrapper_info() const { |
| return &kWrapperInfo; |
| } |
| |
| } // namespace js_injection |