Skip to content

Commit

Permalink
[terminology] renaming raw to blue and sec to red (#80)
Browse files Browse the repository at this point in the history
* renaming raw to blue and sec to red

* Apply suggestions from code review

Co-Authored-By: John-David Dalton <[email protected]>

* JavaScript everywhere

* adding more information to the README.md

* Apply suggestions from code review

Co-Authored-By: John-David Dalton <[email protected]>

Co-authored-by: John-David Dalton <[email protected]>
  • Loading branch information
caridy and jdalton authored Apr 3, 2020
1 parent 309a1c1 commit 27e4a7c
Show file tree
Hide file tree
Showing 12 changed files with 754 additions and 702 deletions.
99 changes: 76 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,117 @@
# Secure Javascript Sandbox
# Sandboxed JavaScript Environment

This is an experimental library to demonstrate that it is possible to use membranes to secure the object graph of a javascript environment without introducing identity discontinuity.
This is an experimental library to demonstrate that it is possible to use membranes to create an object graph of a JavaScript environment without introducing identity discontinuity.

## Goals

* The secure environment must have its own set of intrinsics.
* Code executed inside the secure environment cannot observe the sandbox.
* Mutations on the object graph should only affect the secure environment.
* The sandboxed environment must have its own set of intrinsics.
* Code executed inside the sandboxed environment cannot observe the sandbox.
* Mutations on the object graph should only affect the sandboxed environment.

## Non-goals

* Poisoning is still possible via the membrane by providing object-likes through the membrane that could be used by the outer realm to perform an operation that leaks primitive values that are relevant.
* This library does not provide security guarantees, those must be implemented on top of the distortion mechanism.

## Terminology

In order to make it easier to explain how this library works, we use a color code to identify objects and values in general from both sides of the sandbox:

* Blue Realm is the JavaScript Realm that is not sandboxed.
* Red Realm is the JavaScript Realm that is sandboxed by this library.
* Blue Object, Blue Array, Blue Function, and Blue Values denote values that belong to the Blue Realm.
* Red Object, Red Array, Red Function, and Red Values denote values that belong to the Red Realm.
* Blue Proxy denote a Proxy created in the Blue Realm with a target that belongs to the Red Value.
* Red Proxy denotes a proxy created in the Red Realm with a target being a Blue Value.

## Design

This library implements a membrane to sandbox a JavaScript environment object graph. This membrane is responsible for connecting the Blue Realm with a Red Realm, and it does that by remapping global references in the Red Realm to be Red Proxies (proxies of Blue Values).

This membrane modulates the communication between the two sides, specifically by creating proxies around objects and functions, while letting other primitives values travel safely throughout the membrane.

Arrays never travel through the membrane to mitigate them being used as a communication channel between the two sides of the membrane. Instead, a new Blue Array will be created when a Red Array is passed through the membrane and vise-versa with array items processed individually.

### Cross-sandbox communication

Since you can have multiple sandboxes associated to the Blue Realm, there is a possibility that they communicate with each other. This communication relies on the marshaling principle to avoid wrapping proxies over proxies when values are bounced between sandboxes via the Blue Realm. It does that by preserving the identity of the Blue Proxies observed by the Blue Realm. The Blue Realm is in control at all times, and the only way to communicate between sandboxes is to go through the Blue Realm.

## Implementation Details

This library implements a membrane to sandbox the secure environment object graph, which have intersections with the outer realm object graph. This membrane is implemented by using two type of proxies:
### Implementation in Browsers

In browsers, since we don't have a way to create a light-weight Realm that is synchronously accessible (that will be solved in part by the [stage 2 Realms Proposal](https://github.com/tc39/proposal-realms)), we are forced to use a same-domain iframe in order to isolate the code to be evaluated inside a sandbox for a particular window.

#### Detached iframes

Since the iframes have many ways to reach out to the opener/top window reference, we are forced to use a detached `iframe`, which is, on itself, a complication. A detached `iframe`'s window is a window that does not have any host behavior associated to it, in other words, this window does not have an origin after disconnecting the iframe, which means it can't execute any DOM API without throwing a error. Luckly for us, the JavaScript intrinsics, and all JavaScript language features specified by Ecma262 and Ecma402 are still alive and kicking in that iframe, except for one feature, dynamic imports in a form of `import(specifier)`.

To mitigate the issue with dynamic imports, we are forced to transpile the code that attempts to use this feature of the language, otherwise it will just fail to fetch the module because there is no origin available at the host level. Luckly for us, transpiling dynamic imports is a very common way to bundle code for production systems today.

#### Unforgeables

The `window` reference in the detached iframe, just like any other `window` reference in browsers, contains various unforgeable descriptors, these are descriptors installed in Window, and other globals that are non-configurable, and therefor this library cannot remove them or replace them with a Red Proxy. Must notable, we have the window's prototype chain that is completely unforgeable:

```
window -> Window.prototype -> WindowProperties.prototype -> EventTarget.prototype
```

* A secure proxy, which produces an object that is only accessible from within the secure environment, but "most" operations (but not all) will be performed in the outer realm.
* A reverse proxy, which produces an object that is only accessible from the outer realm, but "all" operations will be performed in the secure environment.
What we do in this case is to keep the identity of those unforgeable around, but changing the descriptors installing on them, and any other method that expects these identities to be passed to them. This make them effectively harmless because they don't give any power.

This concept is extended beyond proxies, and is applicable to other structures, e.g.: Arrays. An Array will never travel through the membrane, instead, a new Array will be created on the other side of the membrane, and Array items will be processed individually, which means no live Arrays can be used as a communication channel between the two sides of the membrane.
Additionally, there are others unforgeables like `location` that are host bounded, in that case, we don't have to do much since the detaching mechanism will automatically invalidate them.

Since you can have multiple secure environment instances in your outer realm, there is a possibility that they communicate with each other. This communication is not modulated, or observed by this library. It relies on the marshaling principle to avoid getting into the infinite loop of proxies for the same object if they are passed from one membrane to another, and it does that my always preserving the identity of the original object or reverse proxy observed by the outer realm.
#### Requirements

The only requirement for the in-browser sandboxing mechanism described above is the usage of `eval` as the main mechanism for evaluating code inside the sandbox. This means your CSP rules should include at least `script-src: 'unsafe-eval'` in order for this library to function.

## Performance

Even though this library is experimental, we want to showcase that it is possible to have a membrane that is fairly fast. The main feature of this library is the laziness aspect of the proxies when accessing outer realm objects and functions from within the secure environment. Those proxies are only going to be initialized when one of the proxy's traps is invoked the first time. This allow us to have a environment creation process that is extremely fast.
Even though this library is still experimental, we want to showcase that it is possible to have a membrane that is fairly fast. The main feature of this library is the laziness aspect of the Red Proxies. Those proxies are only going to be initialized when one of the proxy's traps is invoked the first time. This allow us to have a sandbox creation process that is extremely fast.

Additionally, since existing host javascript environments are immense due the the amount of APIs that they offer, most programs will only need a very small subset of those APIs, and this library only activate the portions of the object graph that are observed by the executed code, making it really light weight compared to other implementations.
Additionally, since existing host JavaScript environments are immense due the the amount of APIs that they offer, most programs will only need a very small subset of those APIs, and this library only activate the portions of the object graph that are observed by the executed code, making it really light weight compared to other implementations.

Finally, reverse proxies are not lazy, they are initialized the first time they go through the membrane even if they are not used by the outer realm. This could be changed in the future if it becomes a bottleneck.
Finally, Blue Proxies are not lazy, they are initialized the first time they go through the membrane even if they are not used by the Blue Realm. This could be changed in the future if it becomes a bottleneck. For now, since this is a less common case, it seems to be fine.

## Where can I use this library?

We do not know the applications of this library just yet, but we suspect that there are many scenarios where it can be useful. Here are some that we have identified:

* Sandbox for polyfills: if you need to evaluate code that requires different set of polyfills and environment configuration, you could sandbox it.
* Limiting capabilities: if you need to evaluate code that should not have access to certain capabilities (global objects, getter, setters, etc.) you could sandbox it with a set of distortions and a whitelist of global properties.
* Sandbox code to preserve the integrity of the app creating the sandbox, all code inside the sandbox will not observe that it is being sandboxed, but will not cause any integrity change that can cause the app's code to malfunction.
* Sandbox for polyfills: if you need to evaluate code that requires different set of polyfills and environment configuration, you could sandbox it without distortions.
* Limiting capabilities: if you need to evaluate code that should not have access to certain capabilities (global objects, getter, setters, etc.) you could sandbox it with a set of distortions to accommodate such limitations.
* Time-sensitive: If you need to evaluate code that should not observe time or should simulate a different time-frame, you should sandbox it with a set of distortions that can adjust the timers.

## Challenges

* Debugging is still very challenging considering that dev-tools are still caching up with the Proxies. Chrome for example has differences displaying proxies in the console vs the watch panel.

## The Code

* This library is implemented using TypeScript, and produces the proper TypeScript types, in case you care about it.
* As today, it does not produce a commonjs or script distribution, it only produces the ES Modules distribution that can be used via www.pika.dev or similar services, or by using the experimental module flag in nodejs 12.x, or above.
* No tests are provided just yet, the plan is to rely on existing tests (e.g.: WPT or ecma262) to validate that the membrane created by this library is a high-fidelity membrane.
* Few tests are provided as of now, but the plan is to rely on existing tests (e.g.: WPT or ecma262) to validate that the membrane created by this library is a high-fidelity membrane.
* The `examples/` folder contains a set of examples showcasing how to use this library.
* The `src/` folder contains the library code, while the `lib/` folder will contain the compiled ES Modules after executing the `build` script from `package.json`.
* This library does not have any runtime dependency, in fact it is very tiny &lt;2kb.

## Open Questions

* Should we proxify Arrays objects to support live Arrays?
* There is not a clear boundary on what can be mutated and what not through the membrane.
* Should we map all intrinsics or only undeniable intrinsics?

## Challenges

### Debuggability

* Debugging is still very challenging considering that dev-tools are still caching up with the Proxies. Chrome for example has differences displaying proxies in the console vs the watch panel.

Additionally, there is an existing bug in ChromeDev-tools that prevent a detached iframe to be debugged (https://bugs.chromium.org/p/chromium/issues/detail?id=1015462).

### WindowProxy

The `window` reference in the iframe, just like any other `window` reference in browsers, exhibit a bizarre behavior, the `WindowProxy` behavior. This has two big implications for this implementation when attempting to give access to other window references coming from same domain iframes (e.g.: sandboxing the main app + one iframe):

* each window will require a new detached iframe to sandbox each of them, but if the iframe navigates to another page, the window reference remains the same, but the internal of the non-observable real window are changing. Otherwise distortions defined for the sandbox will not apply to the identity of the methods from the same-domain iframe.
* GCing the sandbox when the iframe navigates out is tricky due to the fact that the original iframe's window reference remains the same, and it is used by few of the internal maps.

For those reasons, we do not support accessing other realm instances from within the sandbox at the moment.

## Browsers Support and Stats

* Modern browsers with support for ES6 Proxy
* Modern browsers with support for ES6 Proxy and WeakMaps.
* This library: ~3kb minified/gzip for browsers, ~2kb for node (no external dependencies).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@caridy/sjs",
"version": "0.2.5",
"description": "Experimental JS Library to create a secure javascript sandbox in browsers and node",
"description": "Experimental JS Library to create a sandboxed JavaScript environment in browsers and node",
"module": "lib/index.js",
"types": "types/index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('The Error Boundary', () => {
sandboxedValue();
}).toThrowError(RangeError);
});
it('should remap the Outer Realm Error instance to the sandbox errors', function() {
it('should remap the Blue Realm Error instance to the sandbox errors', function() {
expect.assertions(3);
const evalScript = createSecureEnvironment();

Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/freezing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('Freezing', () => {
globalThis.bar = { a: 1, b: 2 };
Object.freeze(globalThis.bar)
const evalScript = createSecureEnvironment();
// checking the state of bar in the outer realm
// checking the state of bar in the blue realm
expect(Object.isExtensible(globalThis.bar)).toBe(false);
expect(Object.isSealed(globalThis.bar)).toBe(true);
expect(Object.isFrozen(globalThis.bar)).toBe(true);
Expand Down Expand Up @@ -56,7 +56,7 @@ describe('Freezing', () => {
expect(Object.isSealed(globalThis.baz)).toBe(false);
expect(Object.isFrozen(globalThis.baz)).toBe(false);
`);
// freezing the raw value after being observed by the sandbox
// freezing the blue value after being observed by the sandbox
Object.freeze(globalThis.baz);
expect(Object.isExtensible(globalThis.baz)).toBe(false);
expect(Object.isSealed(globalThis.baz)).toBe(true);
Expand All @@ -75,7 +75,7 @@ describe('Freezing', () => {
describe('reverse proxies', () => {
it('can be freeze', () => {
expect.assertions(8);
globalThis.outerObjectFactory = function (o: any, f: () => void) {
globalThis.blueObjectFactory = function (o: any, f: () => void) {
expect(Object.isFrozen(o)).toBe(false);
expect(Object.isFrozen(f)).toBe(false);
Object.freeze(o);
Expand All @@ -91,7 +91,7 @@ describe('Freezing', () => {
'use strict';
const o = { x: 1 };
const f = function() {};
outerObjectFactory(o, f);
blueObjectFactory(o, f);
expect(Object.isFrozen(o)).toBe(true);
expect(Object.isFrozen(f)).toBe(true);
expect(() => {
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('SecureEnvironment', () => {
describe('reverse proxies', () => {
it('should not have identity discontinuity for arrays', function() {
expect.assertions(6);
(globalThis as any).outerArrayFactory = function (a1: any, a2: any) {
(globalThis as any).blueArrayFactory = function (a1: any, a2: any) {
expect(Array.isArray(a1)).toBe(true);
expect(a1 instanceof Array).toBe(true);
expect(a1).toStrictEqual([1, 2]);
Expand All @@ -13,11 +13,11 @@ describe('SecureEnvironment', () => {
expect(a2).toStrictEqual([3, 4]);
}
const evalScript = createSecureEnvironment();
evalScript(`outerArrayFactory([1, 2], new Array(3, 4))`);
evalScript(`blueArrayFactory([1, 2], new Array(3, 4))`);
});
it('should not have identity discontinuity for objects', function() {
expect.assertions(6);
(globalThis as any).outerObjectFactory = function (b1: any, b2: any) {
(globalThis as any).blueObjectFactory = function (b1: any, b2: any) {
expect(typeof b1 === 'object').toBe(true);
expect(b1 instanceof Object).toBe(true);
expect(b1.x).toBe(1);
Expand All @@ -26,10 +26,10 @@ describe('SecureEnvironment', () => {
expect(b2.x).toBe(2);
}
const evalScript = createSecureEnvironment();
evalScript(`outerObjectFactory({ x: 1 }, Object.create({}, { x: { value: 2 } }))`);
evalScript(`blueObjectFactory({ x: 1 }, Object.create({}, { x: { value: 2 } }))`);
});
});
describe('secure proxies', () => {
describe('red proxies', () => {
globalThis.foo = {
a1: [1, 2],
a2: new Array(3, 4),
Expand Down
Loading

0 comments on commit 27e4a7c

Please sign in to comment.