The Standards and Web Platform team at Salesforce is championing the creation new features running natively in the web browsers. This post will describe how the new EcmaScript’s ShadowRealm API will improve Salesforce’s security and integrity mechanisms, and how it can be used as a building block for virtualization frameworks, such as the Lightning Web Security.

As with any platform, the web expects its many applications to be composed in many different ways, which makes plenty of room for creative experiences from multiple sources. When the Salesforce Platform embraces the web, it takes the idea of collective composition to its core. This happens when our customers shape the way they use Salesforce with their most creative customizations and share their experience with other customers. Those compositions end up in programs with multiple sources, whether from different teams or vendors, and with a multitude of environment requirements — all of them simultaneously connected to the Salesforce Platform.

In web applications, all things are shared within a single root global environment, which is represented by the main Window object. This is where the core of the Salesforce application runs in the browser and connects to many of its components, including those tailored by customers. It’s fundamental to preserve the integrity of this execution as a whole as it is to maintain security within each piece used for a customer composition.

Applications can set footprints over the global scope and at the available built-in objects. These modifications may vary from adding names to the global (e.g., $ for jQuery) to patching common methods (e.g., adding a custom behavior to Array.prototype.sort). These modifications may impact the integrity of an application that is composed by many components and/or libraries.

What is ShadowRealm?

ShadowRealm is designed to enable creation of a new evaluation context with its own new Global object and its own set of built-in objects, providing a mechanism for executing JavaScript code without sharing or tainting global resources with other parts within that application.

This mechanism executes JavaScript code in the same heap as the surrounding context where the ShadowRealm context is created. The code is executed synchronously, which allows virtualizing the DOM APIs accessed by this code running inside the ShadowRealm instance. Virtualization frameworks rely on this mechanism for the communication between the ShadowRealm and the Window elements.

const sr = new ShadowRealm();

// Sets a new global within the ShadowRealm only
sr.evaluate('globalThis.x = "my shadowRealm"');

globalThis.x = "root"; //

const srx = sr.evaluate('globalThis.x');

srx; // "my shadowRealm"
x; // "root"

A ShadowRealm instance can only transfer JavaScript primitive values — String, Number, BigInt, Symbol, Boolean, undefined, and null — and disallows transferring any objects across the realm boundary. This is important because objects carry identity references of the realm where they were created, which could be used to leak information or cause identity discontinuity issues. For example, an Array constructor from the main Window is different from an Array constructor from any other realm, including a ShadowRealm. Array objects are instances of their respective realm constructor only!

However, ShadowRealm instances can wrap and “share” function values. This enables robust communication channels back and forth between shadow realms.

const sr = new ShadowRealm();

const wrappedFn = sr.evaluate('(x) => globalThis.foo = x');

wrappedFn(42);

globalThis.foo; // undefined

sr.evaluate('globalThis.foo'); // 42

Wrapped functions can also send other functions to be wrapped in the receiving realm.

const sr = new ShadowRealm();

const wrappedFn = sr.evaluate('(x) => globalThis.foo = x');

// The wrapped function received by the shadowRealm will chain the call
// to the arrow function created in this root realm
wrappedFn((y) => globalThis.bar * y);

// The shadow realm just wrapped the arrow function above and
// set it as the value of its respective globalThis.foo

globalThis.bar = 3;
sr.evaluate('globalThis.bar = 0');

// When the sr's `foo` is called, it will call the arrow function 
// in this realm and reflect its return vaue.
sr.evaluate('globalThis.foo(2)'); // 6

This communication is synchronous and can be used to get immediate status of elements in the page or the accurate state of an application.

As the global object is not shared, there are fewer issues of multiple components setting custom, unexpected values on standard built-in objects:

const sr = new ShadowRealm();

// Removes the Array constructor from the global object within this instance
sr.evaluate('delete globalThis.Array;'); // true

// The current Array constructor is not affected
typeof Array; // "function"

Using ShadowRealm with no string evaluation

ShadowRealm introduces no new mechanism to evaluate JavaScript code. ShadowRealm.prototype.evaluate operates similarly to an indirect eval(), with the code running in the respective ShadowRealm instance. This means that the code evaluation is subject to the existing Content Security Policy (CSP) just like the main page. For example, unsafe-eval prevents usage of ShadowRealm.prototype.evaluate.

If the code string evaluation is not possible, there is another way to inject code into a ShadowRealm instance. ShadowRealm.prototype.importValue allows a dynamic module import — as in the import() expression — to load a module and capture an export value, including wrapped functions.

const sr = new ShadowRealm();

const specifier = './foo.js';
const name = 'sum';

// importValue returns a promise that will eventually be resolved with
// the value specified in the given module name.
const shadowSum = await sr.importValue(specifier, name);

shadowSum(1); // runs an operation within the shadowRealm and captures the result

The modules are evaluated per realm, meaning modifications won’t share values or observe globals set from different realms. For example, the module from ./foo.js has the following code:

globalThis.total = 0;

export function sum(n) {
  return globalThis.total += n;
}

export function getTotal() {
  return globalThis.total;
}

In this case, shadowSum is a function that wraps sum loaded inside the shadow realm and — when called — it only affects the globalThis.total from within the respective shadowRealm. In the same way, if the module ./foo.js is loaded in another realm, such as the page realm, the sum function will observe the respective module it was loaded at.

const sr = new ShadowRealm();

const specifier = './foo.js';
const name = 'sum';

const [ shadowSum, shadowGetTotal ] = await Promise.all([
    sr.importValue(specifier, name),
    sr.importValue(specifier, 'getTotal')
]);

globalThis.total = 0;

shadowSum(10); // 10
shadowSum(20); // 30
shadowSum(30); // 60

globalThis.total; // 0
shadowGetTotal(); // 60

const { sum, getTotal } = await import(specifier);

sum(42); // 42
globalThis.total; // 42

// The value from the shadow realm is preserved
shadowGetTotal(); // 60

The importValue is intentionally designed to require a value to be imported from the given module as a starting point to set a communication channel with the ShadowRealm instance. It does not return any module namespace object, such as the regular import() expression, as the objects are currently not allowed to cross the shadow realm boundary.

As the evaluate method is subject to CSP restrictions, such as unsafe-eval, the importValue method is subject to CSP restrictions set to the page, such as default-src. If this directive is present, the API won’t be able to load code this way.

Low-level code and virtualization

The ShadowRealm API contains only two methods and is designed for use in low-level code. The ShadowRealm can often be used as the base for virtualization or code sandboxing systems. The ShadowRealm API is not designed to be used directly by a final user; it requires a framework layer on top, like Lightning Web Security (LWS). LWS uses a membranes framework to communicate the execution of values and states across the ShadowRealms and the main application Window.

The exposed built-in API surface is also likely to be smaller compared to the top-level Window object seen in the main page or those from iframes. Importantly, the global object in a ShadowRealm instance is an ordinary object with all of its properties configurable, which roughly means any global property can be deleted and directly contrasts with the infamous unforgeable global properties from iframes, such as window.top and window.location. These properties are always present in the globals of iframes and cannot be removed. ShadowRealm prevents this, not only by not adding any unforgeable value, but by also setting rules requiring any of the values provided by the host to be effectively deletable.

The new API sets boundaries for code execution that can take advantage of a clean canvas that is not only modules-ready, but can be properly used in the web for virtualizing DOM manipulation that runs from low to no footprint. That differentiates from code that would run without integrity concerns in a clean page. The ShadowRealm API can be a useful and powerful base for membrane frameworks and other encapsulation pieces available in the web.

Roadmap for ShadowRealms

There is more to improve for ShadowRealm, as plenty of this discussion leans towards lowering the bar to attach a membrane framework, or, perhaps, standardizing a membrane framework directly. Part of this discussion weighs the idea of bringing serialization over objects in order to offer a smart solution that is already present in the web today, such as in web workers. Other discussions include creating mechanisms to wrap and unpack promises, iterators, and async iterators across shadow realm boundaries.

All of these ideas are important and function as a vital piece of the standards process for the web ecosystem. The Standards and Web Platform team at Salesforce continues to work on any further enhancements for ShadowRealm as an important piece of the JavaScript language. TC39 maintain’s a GitHub repo with the ShadowRealms API, explainer, issues threads, and the current rendered version of its specification.


About the authors

Leo Balter (he/him) is a Senior Product Manager driving the Standards and Web Platform forward at Salesforce. As a TC39 delegate, Leo is currently championing the ShadowRealm API to ensure that it becomes part of ECMAScript. While not working, Leo loves to play Jazz standards on one of his many guitars.

Rick Waldron (he/him) is a Lead Software Engineer for the Lightning Web Security framework, and a co-champion of the ShadowRealm API. Rick ensures all the JavaScript standards are very well tested as a lead maintainer of Test262, the official JavaScript test suite. He also plays many of his guitars in his free time.

Stay up to date with the latest news from the Salesforce Developers Blog

Subscribe