diff --git a/browser/brave_wallet/solana_provider_renderer_browsertest.cc b/browser/brave_wallet/solana_provider_renderer_browsertest.cc index 663a280abebf..06c119cf2fdc 100644 --- a/browser/brave_wallet/solana_provider_renderer_browsertest.cc +++ b/browser/brave_wallet/solana_provider_renderer_browsertest.cc @@ -543,6 +543,14 @@ class SolanaProviderRendererTest : public InProcessBrowserTest { ASSERT_TRUE(content::WaitForLoadStop(web_contents(browser))); } + void LoadInternalScriptForCreatingTestData() { + constexpr char BypassSafeBuiltins[] = "$Object = Object"; + // Bypass loading safe builtins because we only use this script to create + // test data. + ASSERT_TRUE(ExecJs(web_contents(browser()), BypassSafeBuiltins)); + ASSERT_TRUE(ExecJs(web_contents(browser()), *g_provider_internal_script)); + } + protected: net::EmbeddedTestServer https_server_; TestBraveContentBrowserClient test_content_browser_client_; @@ -775,7 +783,7 @@ IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, Disconnect) { } IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SignTransaction) { - ASSERT_TRUE(ExecJs(web_contents(browser()), *g_provider_internal_script)); + LoadInternalScriptForCreatingTestData(); const std::string serialized_tx_str = VectorToArrayString(kSerializedTx); const std::string tx = base::StrCat({"(window._brave_solana.createTransaction(new Uint8Array([", @@ -821,7 +829,7 @@ IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SignTransaction) { } IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SignAllTransactions) { - ASSERT_TRUE(ExecJs(web_contents(browser()), *g_provider_internal_script)); + LoadInternalScriptForCreatingTestData(); const std::string serialized_tx_str = VectorToArrayString(kSerializedTx); const std::string txs = base::StrCat( {"([window._brave_solana.createTransaction(new Uint8Array([", @@ -883,7 +891,7 @@ IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SignAllTransactions) { } IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SignAndSendTransaction) { - ASSERT_TRUE(ExecJs(web_contents(browser()), *g_provider_internal_script)); + LoadInternalScriptForCreatingTestData(); const std::string serialized_tx_str = VectorToArrayString(kSerializedTx); const std::string send_options = R"({"maxRetries": 9007199254740991, @@ -1276,7 +1284,6 @@ IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SafeBuiltins) { auto result = EvalJs(web_contents(browser()), R"( async function test() { Object.defineProperties = ()=>{} - $Object.defineProperties = ()=>{} await window.braveSolana.connect() window._brave_solana.createPublickey = ()=>'0x00' window.domAutomationController.send( @@ -1292,7 +1299,6 @@ IN_PROC_BROWSER_TEST_F(SolanaProviderRendererTest, SafeBuiltins) { auto result2 = EvalJs(web_contents(browser()), R"( async function test() { Object.freeze = ()=>{} - $Object.freeze = ()=>{} await window.braveSolana.connect() window._brave_solana.abc = 123 if (window._brave_solana.abc === 123) diff --git a/components/brave_wallet/renderer/js_ethereum_provider.cc b/components/brave_wallet/renderer/js_ethereum_provider.cc index 29d6831ed545..6bc12de98226 100644 --- a/components/brave_wallet/renderer/js_ethereum_provider.cc +++ b/components/brave_wallet/renderer/js_ethereum_provider.cc @@ -38,6 +38,7 @@ static base::NoDestructor g_provider_script(""); constexpr char kEthereum[] = "ethereum"; constexpr char kEmit[] = "emit"; constexpr char kIsBraveWallet[] = "isBraveWallet"; +constexpr char kEthereumProviderScript[] = "ethereum_provider.js"; } // namespace @@ -579,7 +580,7 @@ v8::Local JSEthereumProvider::IsUnlocked() { void JSEthereumProvider::InjectInitScript(bool is_main_world) { blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); if (is_main_world) { - ExecuteScript(web_frame, *g_provider_script); + ExecuteScript(web_frame, *g_provider_script, kEthereumProviderScript); } } diff --git a/components/brave_wallet/renderer/js_solana_provider.cc b/components/brave_wallet/renderer/js_solana_provider.cc index dfe744408521..fa7d8a77917a 100644 --- a/components/brave_wallet/renderer/js_solana_provider.cc +++ b/components/brave_wallet/renderer/js_solana_provider.cc @@ -49,6 +49,8 @@ constexpr char kSolana[] = "solana"; constexpr char kSignature[] = "signature"; constexpr char kSignatures[] = "signatures"; constexpr char kToString[] = "toString"; +constexpr char kSolanaProviderSript[] = "solana_provider.js"; +constexpr char kSolanaProviderInternalSript[] = "solana_provider_internal.js"; } // namespace @@ -164,7 +166,7 @@ void JSSolanaProvider::Install(bool allow_overwrite_window_solana, } blink::WebLocalFrame* web_frame = render_frame->GetWebFrame(); - ExecuteScript(web_frame, *g_provider_script); + ExecuteScript(web_frame, *g_provider_script, kSolanaProviderSript); } gin::ObjectTemplateBuilder JSSolanaProvider::GetObjectTemplateBuilder( @@ -874,7 +876,8 @@ v8::Local JSSolanaProvider::CreatePublicKey( v8::Local context, const std::string& base58_str) { // Internal object for CreatePublicKey and CreateTransaction - ExecuteScript(render_frame()->GetWebFrame(), *g_provider_internal_script); + ExecuteScript(render_frame()->GetWebFrame(), *g_provider_internal_script, + kSolanaProviderInternalSript); const base::Value public_key_value(base58_str); std::vector> args; args.push_back(v8_value_converter_->ToV8Value(public_key_value, context)); @@ -889,7 +892,8 @@ v8::Local JSSolanaProvider::CreateTransaction( v8::Local context, const std::vector serialized_tx) { // Internal object for CreatePublicKey and CreateTransaction - ExecuteScript(render_frame()->GetWebFrame(), *g_provider_internal_script); + ExecuteScript(render_frame()->GetWebFrame(), *g_provider_internal_script, + kSolanaProviderInternalSript); const base::Value serialized_tx_value(serialized_tx); std::vector> args; args.push_back(v8_value_converter_->ToV8Value(serialized_tx_value, context)); diff --git a/components/brave_wallet/renderer/v8_helper.cc b/components/brave_wallet/renderer/v8_helper.cc index 1858a6fc709f..cc902ea9777f 100644 --- a/components/brave_wallet/renderer/v8_helper.cc +++ b/components/brave_wallet/renderer/v8_helper.cc @@ -7,31 +7,15 @@ #include -#include "brave/components/safe_builtins/renderer/safe_builtins.h" +#include "brave/components/safe_builtins/renderer/safe_builtins_helpers.h" #include "gin/converter.h" #include "third_party/blink/public/web/web_local_frame.h" -#include "third_party/blink/public/web/web_script_source.h" #include "v8/include/v8-function.h" #include "v8/include/v8-microtask-queue.h" #include "v8/include/v8-object.h" namespace brave_wallet { -namespace { - -void FreezeAndSetGlobally(const base::StringPiece& name, - v8::Local context, - v8::Local value) { - v8::Local object = v8::Local::Cast(value); - CHECK(value->IsObject()) << name; - - object->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen); - SetProviderNonWritable(context, context->Global(), object, - gin::StringToV8(context->GetIsolate(), name), false); -} - -} // namespace - v8::MaybeLocal GetProperty(v8::Local context, v8::Local object, const base::StringPiece& name) { @@ -100,12 +84,13 @@ v8::MaybeLocal CallMethodOfObject( static_cast(args.size()), args.data()); } -void ExecuteScript(blink::WebLocalFrame* web_frame, const std::string script) { +void ExecuteScript(blink::WebLocalFrame* web_frame, + const std::string& script, + const std::string& name) { if (web_frame->IsProvisional()) return; - web_frame->ExecuteScript( - blink::WebScriptSource(blink::WebString::FromUTF8(script))); + brave::LoadScriptWithSafeBuiltins(web_frame, script, name); } void SetProviderNonWritable(v8::Local context, @@ -131,11 +116,4 @@ void SetOwnPropertyNonWritable(v8::Local context, provider_object->DefineProperty(context, property_name, desc).Check(); } -void InitSafeBuiltinsProtection(v8::Local context) { - brave::SafeBuiltins safe_builtins(context); - // Object - v8::Local object = safe_builtins.GetObjekt(); - FreezeAndSetGlobally("$Object", context, object); -} - } // namespace brave_wallet diff --git a/components/brave_wallet/renderer/v8_helper.h b/components/brave_wallet/renderer/v8_helper.h index 18dcc418ee29..64d07ad24a6e 100644 --- a/components/brave_wallet/renderer/v8_helper.h +++ b/components/brave_wallet/renderer/v8_helper.h @@ -40,7 +40,9 @@ v8::MaybeLocal CallMethodOfObject( const base::StringPiece& method_name, std::vector>&& args); -void ExecuteScript(blink::WebLocalFrame* web_frame, const std::string script); +void ExecuteScript(blink::WebLocalFrame* web_frame, + const std::string& script, + const std::string& name); // By default we allow extensions to overwrite the window.[provider] object // but if the user goes into settings and explicitly selects to use Brave Wallet @@ -55,9 +57,6 @@ void SetOwnPropertyNonWritable(v8::Local context, v8::Local provider_object, v8::Local property_name); -// Prevents built-in prototypes from being overwritten. -void InitSafeBuiltinsProtection(v8::Local context); - } // namespace brave_wallet #endif // BRAVE_COMPONENTS_BRAVE_WALLET_RENDERER_V8_HELPER_H_ diff --git a/components/safe_builtins/renderer/BUILD.gn b/components/safe_builtins/renderer/BUILD.gn index 38652be43620..b36955f765e9 100644 --- a/components/safe_builtins/renderer/BUILD.gn +++ b/components/safe_builtins/renderer/BUILD.gn @@ -2,10 +2,14 @@ source_set("renderer") { sources = [ "safe_builtins.cc", "safe_builtins.h", + "safe_builtins_helpers.cc", + "safe_builtins_helpers.h", ] deps = [ "//base", + "//gin", + "//third_party/blink/public:blink", "//v8", ] } diff --git a/components/safe_builtins/renderer/DEPS b/components/safe_builtins/renderer/DEPS index 194d4904dddc..c2958ab3745e 100644 --- a/components/safe_builtins/renderer/DEPS +++ b/components/safe_builtins/renderer/DEPS @@ -1,3 +1,5 @@ include_rules = [ + "+gin", + "+third_party/blink/public", "+v8/include", ] diff --git a/components/safe_builtins/renderer/safe_builtins_helpers.cc b/components/safe_builtins/renderer/safe_builtins_helpers.cc new file mode 100644 index 000000000000..aa772fa8e9a6 --- /dev/null +++ b/components/safe_builtins/renderer/safe_builtins_helpers.cc @@ -0,0 +1,144 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/safe_builtins/renderer/safe_builtins_helpers.h" + +#include "brave/components/safe_builtins/renderer/safe_builtins.h" +#include "gin/converter.h" +#include "third_party/blink/public/web/web_console_message.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "v8/include/v8-exception.h" +#include "v8/include/v8-function.h" +#include "v8/include/v8-local-handle.h" +#include "v8/include/v8-microtask-queue.h" +#include "v8/include/v8-object.h" + +namespace brave { + +namespace { + +std::string CreateExceptionString(v8::Local context, + const v8::TryCatch& try_catch) { + v8::Local message(try_catch.Message()); + if (message.IsEmpty()) { + return "try_catch has no message"; + } + + std::string resource_name = ""; + if (!message->GetScriptOrigin().ResourceName().IsEmpty()) { + v8::String::Utf8Value resource_name_v8( + context->GetIsolate(), message->GetScriptOrigin().ResourceName()); + resource_name.assign(*resource_name_v8, resource_name_v8.length()); + } + + std::string error_message = ""; + if (!message->Get().IsEmpty()) { + v8::String::Utf8Value error_message_v8(context->GetIsolate(), + message->Get()); + error_message.assign(*error_message_v8, error_message_v8.length()); + } + + int line_number = 0; + auto maybe = message->GetLineNumber(context); + line_number = maybe.IsJust() ? maybe.FromJust() : 0; + + return base::StringPrintf("%s:%d: %s", resource_name.c_str(), line_number, + error_message.c_str()); +} + +v8::Local WrapSource(v8::Isolate* isolate, + v8::Local source) { + v8::EscapableHandleScope handle_scope(isolate); + v8::Local left = gin::StringToV8(isolate, + "(function($Object) {" + "'use strict';"); + v8::Local right = gin::StringToV8(isolate, "\n})"); + return handle_scope.Escape(v8::Local(v8::String::Concat( + isolate, left, v8::String::Concat(isolate, source, right)))); +} + +v8::Local RunScript(v8::Local context, + v8::Local name, + v8::Local code) { + v8::EscapableHandleScope handle_scope(context->GetIsolate()); + v8::Context::Scope context_scope(context); + + v8::MicrotasksScope microtasks(context->GetIsolate(), + v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::TryCatch try_catch(context->GetIsolate()); + try_catch.SetCaptureMessage(true); + v8::ScriptOrigin origin(context->GetIsolate(), name); + v8::ScriptCompiler::Source script_source(code, origin); + v8::Local script; + if (!v8::ScriptCompiler::Compile( + context, &script_source, v8::ScriptCompiler::kNoCompileOptions, + v8::ScriptCompiler::NoCacheReason::kNoCacheBecauseInlineScript) + .ToLocal(&script)) { + blink::WebConsoleMessage::LogWebConsoleMessage( + context, blink::WebConsoleMessage( + blink::mojom::ConsoleMessageLevel::kError, + blink::WebString::FromUTF8( + CreateExceptionString(context, try_catch)))); + return v8::Undefined(context->GetIsolate()); + } + + v8::Local result; + if (!script->Run(context).ToLocal(&result)) { + blink::WebConsoleMessage::LogWebConsoleMessage( + context, blink::WebConsoleMessage( + blink::mojom::ConsoleMessageLevel::kError, + blink::WebString::FromUTF8( + CreateExceptionString(context, try_catch)))); + return v8::Undefined(context->GetIsolate()); + } + + return handle_scope.Escape(result); +} + +void SafeCallFunction(blink::WebLocalFrame* web_frame, + v8::Local context, + const v8::Local& function, + int argc, + v8::Local argv[]) { + v8::HandleScope handle_scope(context->GetIsolate()); + v8::Context::Scope scope(context); + v8::MicrotasksScope microtasks(context->GetIsolate(), + v8::MicrotasksScope::kDoNotRunMicrotasks); + v8::Local global = context->Global(); + if (web_frame) { + web_frame->RequestExecuteV8Function(context, function, global, argc, argv, + {}); + } +} + +} // namespace + +void LoadScriptWithSafeBuiltins(blink::WebLocalFrame* web_frame, + const std::string& script, + const std::string& name) { + v8::Local context = web_frame->MainWorldScriptContext(); + v8::Local wrapped_source(WrapSource( + context->GetIsolate(), gin::StringToV8(context->GetIsolate(), script))); + // Wrapping script in function(...) {...} + v8::Local func_as_value = RunScript( + context, gin::StringToV8(context->GetIsolate(), name), wrapped_source); + if (func_as_value.IsEmpty() || func_as_value->IsUndefined()) { + std::string message = + base::StringPrintf("Bad source for require(\"%s\")", name.c_str()); + web_frame->AddMessageToConsole( + blink::WebConsoleMessage(blink::mojom::ConsoleMessageLevel::kError, + blink::WebString::FromUTF8(message))); + return; + } + + v8::Local func = v8::Local::Cast(func_as_value); + SafeBuiltins safe_builtins(context); + // These must match the argument order in WrapSource. + v8::Local args[] = {safe_builtins.GetObjekt()}; + + SafeCallFunction(web_frame, context, func, std::size(args), args); +} + +} // namespace brave diff --git a/components/safe_builtins/renderer/safe_builtins_helpers.h b/components/safe_builtins/renderer/safe_builtins_helpers.h new file mode 100644 index 000000000000..ced1cd773e9c --- /dev/null +++ b/components/safe_builtins/renderer/safe_builtins_helpers.h @@ -0,0 +1,26 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_SAFE_BUILTINS_RENDERER_SAFE_BUILTINS_HELPERS_H_ +#define BRAVE_COMPONENTS_SAFE_BUILTINS_RENDERER_SAFE_BUILTINS_HELPERS_H_ + +#include + +namespace blink { +class WebLocalFrame; +} // namespace blink + +namespace brave { + +// Load script in a closure that will use safe builtin types to prevent +// prototype pollution attack. When a new type is added, we need to update +// WrapSource and args for SafeCallFunction. +void LoadScriptWithSafeBuiltins(blink::WebLocalFrame* web_frame, + const std::string& script, + const std::string& name); + +} // namespace brave + +#endif // BRAVE_COMPONENTS_SAFE_BUILTINS_RENDERER_SAFE_BUILTINS_HELPERS_H_ diff --git a/renderer/brave_wallet/brave_wallet_render_frame_observer.cc b/renderer/brave_wallet/brave_wallet_render_frame_observer.cc index a47562c0c4e5..6e52c2d8f671 100644 --- a/renderer/brave_wallet/brave_wallet_render_frame_observer.cc +++ b/renderer/brave_wallet/brave_wallet_render_frame_observer.cc @@ -15,8 +15,6 @@ #include "third_party/blink/public/web/blink.h" #include "third_party/blink/public/web/web_local_frame.h" -#include "brave/components/brave_wallet/renderer/v8_helper.h" - namespace brave_wallet { BraveWalletRenderFrameObserver::BraveWalletRenderFrameObserver( @@ -66,7 +64,6 @@ void BraveWalletRenderFrameObserver::DidCreateScriptContext( if (render_frame()->GetWebFrame()->GetDocument().IsDOMFeaturePolicyEnabled( render_frame()->GetWebFrame()->MainWorldScriptContext(), "ethereum")) { - InitSafeBuiltinsProtection(context); if (!js_ethereum_provider_) { js_ethereum_provider_.reset(new JSEthereumProvider( render_frame(), dynamic_params.brave_use_native_ethereum_wallet)); @@ -107,7 +104,6 @@ void BraveWalletRenderFrameObserver::DidClearWindowObject() { base::FeatureList::IsEnabled( brave_wallet::features::kBraveWalletSolanaProviderFeature) && web_frame->GetDocument().IsDOMFeaturePolicyEnabled(context, "solana")) { - InitSafeBuiltinsProtection(context); auto dynamic_params = get_dynamic_params_callback_.Run(); JSSolanaProvider::Install( dynamic_params.allow_overwrite_window_solana_provider, render_frame());