Skip to content

Javascript Binding - Add ability to limit access to JavaScript Bound objects to specific origins #5085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 89 additions & 40 deletions CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "Wrapper\Browser.h"
#include "..\CefSharp.Core.Runtime\Internals\Messaging\Messages.h"
#include "..\CefSharp.Core.Runtime\Internals\Serialization\Primitives.h"
#include <include/cef_parser.h>

using namespace System;
using namespace System::Diagnostics;
Expand Down Expand Up @@ -100,6 +101,16 @@ namespace CefSharp
}

_jsBindingApiEnabled = extraInfo->GetBool("JavascriptBindingApiEnabled");
_jsBindingApiHasAllowOrigins = extraInfo->GetBool("JavascriptBindingApiHasAllowOrigins");

if (_jsBindingApiHasAllowOrigins)
{
auto allowOrigins = extraInfo->GetList("JavascriptBindingApiAllowOrigins");
if (allowOrigins.get() && allowOrigins->IsValid())
{
_jsBindingApiAllowOrigins = allowOrigins->Copy();
}
}

if (extraInfo->HasKey("JsBindingPropertyName") || extraInfo->HasKey("JsBindingPropertyNameCamelCase"))
{
Expand Down Expand Up @@ -149,50 +160,88 @@ namespace CefSharp

if (_jsBindingApiEnabled)
{
//TODO: Look at adding some sort of javascript mapping layer to reduce the code duplication
auto global = context->GetGlobal();
auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier());
auto processId = System::Diagnostics::Process::GetCurrentProcess()->Id;

//TODO: JSB: Split functions into their own classes
//Browser wrapper is only used for BindObjectAsync
auto bindObjAsyncFunction = CefV8Value::CreateFunction(kBindObjectAsync, new BindObjectAsyncHandler(_registerBoundObjectRegistry, _javascriptObjects, browserWrapper));
auto unBindObjFunction = CefV8Value::CreateFunction(kDeleteBoundObject, new RegisterBoundObjectHandler(_javascriptObjects));
auto removeObjectFromCacheFunction = CefV8Value::CreateFunction(kRemoveObjectFromCache, new RegisterBoundObjectHandler(_javascriptObjects));
auto isObjectCachedFunction = CefV8Value::CreateFunction(kIsObjectCached, new RegisterBoundObjectHandler(_javascriptObjects));
auto postMessageFunction = CefV8Value::CreateFunction(kPostMessage, new JavascriptPostMessageHandler(rootObject == nullptr ? nullptr : rootObject->CallbackRegistry));
auto promiseHandlerFunction = CefV8Value::CreateFunction(kSendEvalScriptResponse, new JavascriptPromiseHandler());

//By default We'll support both CefSharp and cefSharp, for those who prefer the JS style
auto createCefSharpObj = !_jsBindingPropertyName.empty();
auto createCefSharpObjCamelCase = !_jsBindingPropertyNameCamelCase.empty();

if (createCefSharpObj)
auto createObjects = true;

if (_jsBindingApiHasAllowOrigins)
{
auto cefSharpObj = CefV8Value::CreateObject(nullptr, nullptr);
cefSharpObj->SetValue(kBindObjectAsync, bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kDeleteBoundObject, unBindObjFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kRemoveObjectFromCache, removeObjectFromCacheFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kIsObjectCached, isObjectCachedFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kPostMessage, postMessageFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kSendEvalScriptResponse, promiseHandlerFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kRenderProcessId, CefV8Value::CreateInt(processId), CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

global->SetValue(_jsBindingPropertyName, cefSharpObj, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY);
createObjects = false;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract this into a helper method would probably make this a lot cleaner


auto frameUrl = frame->GetURL();

CefURLParts frameUrlParts;

if (CefParseURL(frameUrl, frameUrlParts))
{
auto frameUrlOrigin = CefString(frameUrlParts.origin.str, frameUrlParts.origin.length);
auto clrframeUrlOrigin = StringUtils::ToClr(frameUrlOrigin);

auto size = static_cast<int>(_jsBindingApiAllowOrigins->GetSize());

for (int i = 0; i < size; i++)
{
auto origin = _jsBindingApiAllowOrigins->GetString(i);

auto clrOrigin = StringUtils::ToClr(origin);

auto originEqual = String::Compare(clrframeUrlOrigin, clrOrigin, StringComparison::InvariantCultureIgnoreCase);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to find a more efficient string comparison, ideally compare the native strings


if (originEqual == 0)
{
createObjects = true;

break;
}
}
}
}

if (createCefSharpObjCamelCase)
if (createObjects)
{
auto cefSharpObjCamelCase = CefV8Value::CreateObject(nullptr, nullptr);
cefSharpObjCamelCase->SetValue(kBindObjectAsyncCamelCase, bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kDeleteBoundObjectCamelCase, unBindObjFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kRemoveObjectFromCacheCamelCase, removeObjectFromCacheFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kIsObjectCachedCamelCase, isObjectCachedFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kPostMessageCamelCase, postMessageFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kSendEvalScriptResponseCamelCase, promiseHandlerFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kRenderProcessIdCamelCase, CefV8Value::CreateInt(processId), CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

global->SetValue(_jsBindingPropertyNameCamelCase, cefSharpObjCamelCase, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY);
//TODO: Look at adding some sort of javascript mapping layer to reduce the code duplication
auto global = context->GetGlobal();
auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier());
auto processId = System::Diagnostics::Process::GetCurrentProcess()->Id;

//TODO: JSB: Split functions into their own classes
//Browser wrapper is only used for BindObjectAsync
auto bindObjAsyncFunction = CefV8Value::CreateFunction(kBindObjectAsync, new BindObjectAsyncHandler(_registerBoundObjectRegistry, _javascriptObjects, browserWrapper));
auto unBindObjFunction = CefV8Value::CreateFunction(kDeleteBoundObject, new RegisterBoundObjectHandler(_javascriptObjects));
auto removeObjectFromCacheFunction = CefV8Value::CreateFunction(kRemoveObjectFromCache, new RegisterBoundObjectHandler(_javascriptObjects));
auto isObjectCachedFunction = CefV8Value::CreateFunction(kIsObjectCached, new RegisterBoundObjectHandler(_javascriptObjects));
auto postMessageFunction = CefV8Value::CreateFunction(kPostMessage, new JavascriptPostMessageHandler(rootObject == nullptr ? nullptr : rootObject->CallbackRegistry));
auto promiseHandlerFunction = CefV8Value::CreateFunction(kSendEvalScriptResponse, new JavascriptPromiseHandler());

//By default We'll support both CefSharp and cefSharp, for those who prefer the JS style
auto createCefSharpObj = !_jsBindingPropertyName.empty();
auto createCefSharpObjCamelCase = !_jsBindingPropertyNameCamelCase.empty();

if (createCefSharpObj)
{
auto cefSharpObj = CefV8Value::CreateObject(nullptr, nullptr);
cefSharpObj->SetValue(kBindObjectAsync, bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kDeleteBoundObject, unBindObjFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kRemoveObjectFromCache, removeObjectFromCacheFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kIsObjectCached, isObjectCachedFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kPostMessage, postMessageFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kSendEvalScriptResponse, promiseHandlerFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObj->SetValue(kRenderProcessId, CefV8Value::CreateInt(processId), CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

global->SetValue(_jsBindingPropertyName, cefSharpObj, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY);
}

if (createCefSharpObjCamelCase)
{
auto cefSharpObjCamelCase = CefV8Value::CreateObject(nullptr, nullptr);
cefSharpObjCamelCase->SetValue(kBindObjectAsyncCamelCase, bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kDeleteBoundObjectCamelCase, unBindObjFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kRemoveObjectFromCacheCamelCase, removeObjectFromCacheFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kIsObjectCachedCamelCase, isObjectCachedFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kPostMessageCamelCase, postMessageFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kSendEvalScriptResponseCamelCase, promiseHandlerFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);
cefSharpObjCamelCase->SetValue(kRenderProcessIdCamelCase, CefV8Value::CreateInt(processId), CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

global->SetValue(_jsBindingPropertyNameCamelCase, cefSharpObjCamelCase, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ namespace CefSharp
bool _focusedNodeChangedEnabled;
bool _legacyBindingEnabled;
bool _jsBindingApiEnabled = true;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing these on a per browser basis would provide greater flexibility, we could create a class that has a lookup by browser id.

bool _jsBindingApiHasAllowOrigins = false;
CefRefPtr<CefListValue> _jsBindingApiAllowOrigins;

// The property names used to call bound objects
CefString _jsBindingPropertyName;
Expand Down Expand Up @@ -69,6 +71,8 @@ namespace CefSharp
}
delete _onBrowserCreated;
delete _onBrowserDestroyed;

_jsBindingApiAllowOrigins = nullptr;
}

CefBrowserWrapper^ FindBrowserWrapper(int browserId);
Expand Down
16 changes: 16 additions & 0 deletions CefSharp.Core.Runtime/ManagedCefBrowserAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ namespace CefSharp

extraInfo->SetBool("JavascriptBindingApiEnabled", objectRepositorySettings->JavascriptBindingApiEnabled);

auto hasJavascriptBindingApiAllowOrigins = objectRepositorySettings->HasJavascriptBindingApiAllowOrigins();

extraInfo->SetBool("JavascriptBindingApiHasAllowOrigins", hasJavascriptBindingApiAllowOrigins);

if (hasJavascriptBindingApiAllowOrigins)
{
auto allowOriginList = CefListValue::Create();

for (int i = 0; i < objectRepositorySettings->JavascriptBindingApiAllowOrigins->Length; i++)
{
allowOriginList->SetString(i, StringUtils::ToNative(objectRepositorySettings->JavascriptBindingApiAllowOrigins[i]));
}

extraInfo->SetList("JavascriptBindingApiAllowOrigins", allowOriginList);
}

CefRefPtr<CefRequestContext> requestCtx;

if (requestContext != nullptr)
Expand Down
1 change: 1 addition & 0 deletions CefSharp.Test/CefSharpFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private void CefInitialize()
settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Tests\\Cache");
settings.RootCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Tests");
//settings.CefCommandLineArgs.Add("renderer-startup-dialog");
//settings.CefCommandLineArgs.Add("disable-features=SpareRendererForSitePerProcess");
//settings.CefCommandLineArgs.Add("disable-site-isolation-trials");
settings.SetOffScreenRenderingBestPerformanceArgs();

Expand Down
54 changes: 54 additions & 0 deletions CefSharp.Test/JavascriptBinding/JavascriptBindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,60 @@ public async Task ShouldDisableJsBindingApi()
}
}

[Fact]
public async Task ShouldDisableJsBindingApiForOrigin()
{
using (var browser = new ChromiumWebBrowser(CefExample.BindingApiCustomObjectNameTestUrl, automaticallyCreateBrowser: false))
{
var settings = browser.JavascriptObjectRepository.Settings;
settings.JavascriptBindingApiEnabled = true;
settings.JavascriptBindingApiAllowOrigins = new string[] { "notallowed" };

//To modify the settings we need to defer browser creation slightly
browser.CreateBrowser();

var loadResponse = await browser.WaitForInitialLoadAsync();

Assert.True(loadResponse.Success);

var response1 = await browser.EvaluateScriptAsync("typeof window.cefSharp === 'undefined'");
var response2 = await browser.EvaluateScriptAsync("typeof window.CefSharp === 'undefined'");

Assert.True(response1.Success);
Assert.True((bool)response1.Result);

Assert.True(response2.Success);
Assert.True((bool)response2.Result);
}
}

[Theory]
[InlineData(CefExample.BaseUrl + "/")]
[InlineData("someorigin", CefExample.BaseUrl + "/")]
public async Task ShouldEnableJsBindingApiForOrigin(params string[] origins)
{
using (var browser = new ChromiumWebBrowser(CefExample.BindingApiCustomObjectNameTestUrl, automaticallyCreateBrowser: false))
{
var settings = browser.JavascriptObjectRepository.Settings;
settings.JavascriptBindingApiEnabled = true;
settings.JavascriptBindingApiAllowOrigins = origins;

//To modify the settings we need to defer browser creation slightly
browser.CreateBrowser();

await browser.WaitForInitialLoadAsync();

var response1 = await browser.EvaluateScriptAsync("typeof window.cefSharp === 'undefined'");
var response2 = await browser.EvaluateScriptAsync("typeof window.CefSharp === 'undefined'");

Assert.True(response1.Success);
Assert.False((bool)response1.Result);

Assert.True(response2.Success);
Assert.False((bool)response2.Result);
}
}

[Fact]
public async Task ShouldEnableJsBindingApi()
{
Expand Down
32 changes: 32 additions & 0 deletions CefSharp/JavascriptBinding/JavascriptBindingSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System.Collections.Generic;
using CefSharp.Internals;

namespace CefSharp.JavascriptBinding
Expand All @@ -15,6 +16,7 @@ public class JavascriptBindingSettings : FreezableBase
private bool legacyBindingEnabled;
private string jsBindingGlobalObjectName;
private bool jsBindingApiEnabled = true;
private string[] javascriptBindingApiAllowOrigins;

/// <summary>
/// The Javascript methods that CefSharp provides in relation to JavaScript Binding are
Expand All @@ -33,6 +35,24 @@ public bool JavascriptBindingApiEnabled
}
}

/// <summary>
/// When <see cref="JavascriptBindingApiEnabled"/> is set to true, set a collection
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs review

/// of origins to limit which origins CefSharp will create it's global (window) object.
/// </summary>
/// <remarks>
/// If you wish to create the CefSharp object for a limited set of origins then set this property
/// </remarks>
public string[] JavascriptBindingApiAllowOrigins
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The origin returned from CEF when it's parsed always has a trailing /, we should ensure that there's always a trailing slash programmatically

{
get { return javascriptBindingApiAllowOrigins; }
set
{
ThrowIfFrozen();

javascriptBindingApiAllowOrigins = value;
}
}

/// <summary>
/// The Javascript methods that CefSharp provides in relation to JavaScript Binding are
/// created using a Global (window) Object. Settings this property allows you to customise
Expand Down Expand Up @@ -95,5 +115,17 @@ public bool AlwaysInterceptAsynchronously
alwaysInterceptAsynchronously = value;
}
}

/// <summary>
/// HasJavascriptBindingApiAllowOrigins
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs review.

/// </summary>
/// <returns>bool true if <see cref="JavascriptBindingApiAllowOrigins"/> is non empty collection.</returns>
public bool HasJavascriptBindingApiAllowOrigins()
{
if (javascriptBindingApiAllowOrigins == null)
return false;

return javascriptBindingApiAllowOrigins.Length > 0;
}
}
}