blob: 6cd4cda9d2f31d59d821fddd7cf89978f6891491 [file] [log] [blame]
// 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