Giter Site home page Giter Site logo

proposal-ses's Introduction

Draft Proposal for SES (Secure EcmaScript)

  • Most of the action on the SES-shim is happening at https://github.com/Agoric/SES-shim/tree/master/packages/ses .
  • Moddable is also directly building a SES machine as the primary configuration of the XS implementation of JS for embedded, as standardized in Ecma TC53.
  • The most relevant proposal these days is https://github.com/tc39/proposal-compartments which is much more up to date than this proposal repository is.
  • We should update this proposal repository with text from https://github.com/Agoric/SES-shim/tree/master/packages/ses which is much more current.
  • MetaMask and Agoric both have plugin architectures running on SES.
  • MetaMask and MetaMask's LavaMoat run on the SES-shim.
  • The Agoric framework runs on both on the SES-shim on Node, and increasingly on a version of XS called XSnap that also supports orthogonal persistence.

Note that this proposal was previously called "proposal-frozen-realms". However, with progress on proposal-realms, the realms-shim, and the ses-shim, we found we no longer needed to distinguish frozen-realms from SES. Most historical references to "Frozen Realms" are best interpreted as being about an older version of SES.

Champions

  • Mark S. Miller, Agoric
  • JF Paradis, Agoric
  • Caridy Patiño, Salesforce
  • Patrick Soquet, Moddable
  • Bradley Farias, GoDaddy, Node

This document specifies "compartments", a concept focused on making lightweight realms designed to be used with a shared immutable realm. The proposal here is intended to compose well with the various Realm proposals but is independent. These proposals each have utility without the other, and so can be proposed separately. However, together they have more power than each separately.

We motivate the SES API presented here with a variety of examples.

Status

Current Stage:

  • Stage 1

External links

Moddable's Compartment API, the direct ancestor to this proposal, implemented on the XS SES engine.

Making Javascript Safe and Secure Talks by Mark S. Miller (Agoric), Peter Hoddie (Moddable), and Dan Finlay (MetaMask)

Presentation to TC53 Omit Needless Words

LavaMoat - Securing your dependency graph by Kumavis (MetaMask)

Presentation to Node Security Securing EcmaScript

Historical

Automated Analysis of Security-Critical JavaScript APIs by Ankur Taly Úlfar Erlingsson John C. Mitchell Mark S. Miller Jasvir Nagra

Frozen Realms: Draft Standard Support for Safer JavaScript Plugins is an in-depth talk that covers the important ideas, but is very stale regarding specifics.

The old Realms API proposal and the current Realms proposal. The original plan was to settle the Realms proposal first, but with the current approach, this is no longer required.

The original efforts to rebuild frozen realms on top of these Realms is:

Summary

In ECMAScript, a realm consists of a global object and an associated set of primordial objects -- mutable objects like Array.prototype that must exist before any code runs. Objects within a realm implicitly share these primordials and can therefore easily disrupt each other by primordial poisoning -- modifying these objects to behave badly. This disruption may happen accidentally or maliciously. Today, in the browser, realms can be created via same origin iframes, and in Node via vm contexts. On creation, these realms are separate from each other because they share no mutable state. Because prototypes are mutable, each realm needs its own set, making this separation too expensive to be used at fine grain.

Realms are currently not exposed directly to JavaScript but are represented in the specs by the realm record, of which the most important slots are the intrinsics, the global object, and the global lexical environment (see ECMA262 sections 8.2 Realms).

We propose to add the concept of compartments, to designate lightweight child realms inside a realm. Each compartment has its own global object and global lexical scope, but all compartments inside a given realm share their intrinsics. Separation is achieved by making the intrinsics immutable, preventing an object in one compartment from poisoning the prototypes used by the other compartments.

This means that each compartment consists of a new global object, and a new global lexical environment:

Record slots Realm Compartment
intrinsics mutable immutable, shared
global object mutable mutable
global lexical scope mutable mutable

The compartment record is like a realm record, except that its intrinsics slot points to the parent realm record. Everywhere the specs refers to the the realm record, the compartment record can be subsituted with no further changes.

The Compartment constructor

Superceded by the Compartments proposal.

We propose a Compartment class, whose instances is a reification of the concept of "compartment" introduced above, for making multiple lightweight child realms inside a given realm.

Though initially separate, compartments can be brought into intimate contact with each other via global object and modules.

class Compartment {
  constructor: (
    endowments: object?,     // extra bindings added to the global object
    moduleMap: object?,      // maps child specifier to parent specifier
    options: object?         // including hooks like isDirectEvalHook
  ) -> object                // an exotic compartment object

  get global -> object       // access this compartment's global object

  evaluate(                  // do a strict indirect eval in this compartment
    src: stringable,
    options: object?         // per-evaluation rather than per-compartment
  ) -> any

  // same signature as dynamic import
  async import(specifier: string) -> promise<ModuleNamespace>
  importSync(specifier: string) -> ModuleNamespace
}

The compartment constructor creates a new lightweight child realm with a new global, a new eval function, a new Function constructor, and a new Compartment constructor.

  • The compartment global object consists of all the primordial state defined by ECMA262, but contains no host provided objects, so window, document, XMLHttpRequest, require, process etc. are all absent. Thus, a compartment contains none of the objects needed for interacting with the outside world, like the user or the network.

  • The new eval, Function, and Compartment will evaluate code in the global scope of the new compartment: the new compartment's global becomes their global object.

  • The new eval, Function, and Compartment inherit from the shared %FunctionPrototype%.

  • The new Function.prototype is the shared %FunctionPrototype%.

  • The new Compartment constructor...?

  • The new Compartment.prototype is the shared %CompartmentPrototype%.

The constructor then copies the values of the own enumerable properties from the endowments parameter onto the new global and returns the new compartment instance. With these additional endowments, users provide the virtual host objects that they wish to be available in the spawned compartment.

The Compartment constructor is only available on the global object after lockdown has been invoked (see below).

The Compartment prototype

We propose on the shared Compartment.prototype, to be inherited by instances of the all Compartment classes:

  • a global getter to provide access to the compartment global object. Its behavior is similar to the globalThis global object.
  • an evaluate method to evaluate code in the global scope of the new compartment. Its signature is identical to the eval() function but possibly with an additional optional options argument.
  • an asynchronous import method to dynamically load modules in the new compartment. Its signature is identical to the dynamic import function.

The lockdown method

We propose a static method, lockdown() or Realm.lockdown(), for converting the current realm into a state with immutable primordials. We call such a realm an immutable realm. The Realm global object will be specified by the Realms proposal.

The lockdown operation consists of:

  • taming some globals (see below).
  • taming the function constructors (see below).
  • freezing all intrinsics (see below).
  • disabling the default mechanism causing the override mistake (see below).
  • exposing the Compartment constructor via the global object which is not available before lockdown (see below).

Although Compartment and Realm.lockdown() appear orthogonal, they are only interesting when directly composed:

Realm.lockdown();
const cmpA = new Compartment();
const cmpB = new Compartment();

After lockdown, all the primordials that cmpA and cmpB share are immutable, so neither can poison the prototypes of the other. Because they share no mutable state, they are as fully separate from each other as two full realms created by two same origin iframes (except the shared identity of frozen primordials, thus avoiding identity discontinuity explained below).

Modification of the prototypes is allowed before lockdown is called (which raises interesting issues re what is frozen by lockdown).

(edit with next two paragraphs)

A long recognized best practice is "don't monkey-patch primordials" -- don't mutate any primordial state. Most legacy code obeying this practice is already compatible with lightweight realms descending from an immutable root realm. Some further qualifications are explained in the rest of this document.

If customization of the intrinsics is required, it can be done before lockdown is called and before any compartment is created.

The Compartment global object

The compartment constructor is unavailable before lockdown() is called, to avoid the risk of omitting lockdown and creating compartments with non-frozen primordials (which would not provide the intended isolation).

Freezing intrinsics and Taming globals

In order for the intrinsics to be shared safely, they must be transitively immutable. Fortunately, of the standard primordials in ES2016, the only mutable primordial state is:

  • Mutable own properties of primordial objects
  • The mutable internal [[Prototype]] slot of primordial objects
  • The ability to add properties
  • Math.random
  • Date.now
  • The Date constructor called as a constructor with no arguments
  • The Date constructor called as a function, no matter what arguments (Surprised me!)
  • Normative optional proposed RegExp static methods (link)
  • Normative optional proposed Error.prototype.stack accessor (link)

To make a transitively immutable root realm, we, respectively

  • Remove all non-standard properties
  • Remove Math.random
  • Remove Date.now
  • Have new Date() throw a TypeError
  • Have Date(any...) throw a TypeError
  • Remove the RegExp static methods if present
  • Remove RegExp.prototype.compile
  • Remove Error.prototype.stack if present
  • Make all primordial objects non-extensible.
  • Make all remaining properties non-configurable, non-writable. If an accessor property, we specify that its getter must always return the same value without mutating any state, and its setter either be absent or throw an error without mutating any state.

Likewise, any new addition to the specifications need to follow the same policy, in order to avoid introducing mutable state in a compartment.

A user can effectively add the missing functionality of Date and Math back in when necessary, or substitute safe implementations. For example

const DateNow = Date.now;

Realm.lockdown();

function unsafeDate() {
  return Date(...arguments);
}
Object.defineProperties(unsafeDate, Object.getOwnPropertyDescriptors(Date));
Object.defineProperty(unsafeDate, 'now', {
	value: DateNow,
	writable: true,
	enumerable: false,
	configurable: true
});

const cmp = new Compartment({ Date: unsafeDate });

Taming the function constructors.

All intrinsics are shared, but the %Function%, %GeneratorFunction%, %AsyncFunction% and %AsyncGeneratorFunction% perform by default source code evaluation in the global scope of the realm.

After lockdown, these constructor should be replaced with functions that throw instead of evaluating source code, so they can be safely shared. We could specify that their throwing behavior is the same as when the host hook (for CSP) suppresses evaluation, mapping it to an already possible behavior. If Compartment is a per-realm global rather than per-Compartment, then Compartment.prototype.constructor === Compartment, which is not tamed? Let's talk about this.

Override mistake

Because of lack of sufficient foresight at the time, ES5 unfortunately specified that a simple assignment to a non-existent property must fail if it would override a non-writable data property of the same name. (In retrospect, this was a mistake, but it is now too late and we must live with the consequences.) It is inconsistent with overriding by classes and object literals, since they do [[DefineOwnProperty]] rather than assignment.

As a result, simply freezing an object to make it immutable has the unfortunate side effect of breaking previously correct code that is considered to have followed JS best practices, if this previous code used assignment to override. For example this assignment will fail:

Object.freeze(Array.prototype);
const arr = []
arr.join = true; // throws in strict mode, ignore in sloppy mode.

For that reason, after freezing the primordials, we need to Make non-writable prototype properties not prevent assigning to instance.

See the override mistake. (better link?)

(We need another bit of semantic state to distinguish these two ways of being frozen. We should specify that petrify and perhaps even harden also protect against override mistake, even though we avoid fully shimming that.)

Identity discontinuity

Two realms, made by same origin iframes or vm contexts, can be put in contact. Once in contact, they can mix their object graphs freely. When realms do this, they encounter an inconvenience and source of bugs we will here call identity discontinuity. For example if code from iframeA makes an array arr that it passes to code from iframeB, and iframeB tests arr instanceof Array, the answer will be false since arr inherits from the Array.prototype of iframeA which is a different object than the Array.prototype of iframeB.

By contrast, since cmpA and cmpB share the same Array.prototype, an array arr created by one still passes the arr instanceof Array as tested by the other.

###################################

TODO BELOW

Confinement examples

function confine(src, endowments) {
  return sharedRoot.spawn(endowments).eval(src);
}

This confine function is an example of a security abstraction we can easily build by composing the primitives above. It uses spawn to make a lightweight realm descendant from our immutable sharedRoot realm above, copies the own enumerable properties of endowments onto the global of that lightweight realm, and then evaluates src in the scope of that global and returns the result. This confine function is especially useful for object-capability programming. These primitives (together with membranes) can also help to support other security models such as decentralized dynamic information flow though more mechanism may additionally be needed. We have not yet explored this in any detail.

(The confine function is from SES, which has a formal semantics supporting automated verification of some security properties of SES code. It was developed as part of the Google Caja project; you can read more about SES and Caja on the Caja website.)

confine('x + y', {x: 3, y: 4})  // -> 7

confine('Object', {})           // -> Object constructor of an immutable root

confine('window', {})           // ReferenceError, no 'window' in scope

Plugin separation example

function Counter() {
  let count = 0;
  return Object.freeze({
    incr: Object.freeze(() => ++count),
    decr: Object.freeze(() => --count)
  });
}
const counter = new Counter();

// ...obtain billSrc and joanSrc from untrusted clients...
const bill = confine(billSrc, {change: counter.incr});
const joan = confine(joanSrc, {change: counter.decr});

Say the code above is executed by a program we call Alice. Within this code, Alice obtains source code for plugins Bill and Joan. Alice does not know how well these plugins are written, and so wishes to protect herself from their misbehavior, as well as protect each of them from the misbehavior of the other. It does not matter whether Alice is worried about accidental or malicious misbehavior.

With the code above, Alice presents to each of these plugins an API surface of her design, characteristic of the plugin framework she defines. In this trivial example, she provides to each a function they will know as change for manipulating the state of a shared counter. By calling his change variable, Bill can only increment the counter and see the result. By calling her change variable, Joan can only decrement the counter and see the result. By using her counter variable Alice can do both.

If Alice's code above is normal JavaScript code, then she does not achieve this goal. For example, Bill or Joan could use the expression change.__proto__ to access and poison Alice's prototypes, and to interact with each other in ways Alice did not intend to enable. The API surface that Alice exposed to Bill and Joan was not defensive; it did not protect itself and Alice from Bill and Joan's misbehavior.

Alice's code above is properly defensive if it is evaluated in a realm descendant from an immutable root realm. Alice places Bill and Joan in such a realm to confine them. She places herself in such a realm for its defensibility, which Alice can use to define defensive abstractions that are safe to expose to Bill and Joan. If Alice, Bill, and Joan all descend from sharedRoot, then their further interactions are defensible and free of identity discontinuities.

A convenience: def(obj)

All those calls to Object.freeze above are ugly. The Caja def(obj) function is an example of a convenience that should be provided by a library. It applies Object.freeze recursively to all objects it finds starting at obj by following property and [[Prototype]] links. This gives all these objects a tamper proof API surface (Note, though, that it does not make them immutable except in special cases.) The name def means "define a defensible object".

Using def, we can rewrite our Counter example code as

function Counter() {
  let count = 0;
  return def({
    incr() { return ++count; }
    decr() { return --count; }
  });
}

To be efficient, def needs to somehow be in bed with this proposal, so it can know to stop traversing when it hits any of these transitively immutable primordials. We leave it to a later proposal to work out this integration issue.

Compartments example

By composing revocable membranes and confine, we can make compartments:

function makeCompartment(src, endowments) {
  const {wrapper,
         revoke} = makeMembrane(confine);
  return {wrapper: wrapper(src, endowments),
          revoke};
}

// ...obtain billSrc and joanSrc from untrusted clients...
const {wrapper: bill,
       revoke: killBill} = makeCompartment(billSrc, endowments);
const {wrapper: joan,
       revoke: killJoan} = makeCompartment(joanSrc, endowments);

// ... introduce mutually suspicious Bill and Joan to each other...
// ... use both ...
killBill();
// ... Bill is inaccessible to us and to Joan. GC can collect Bill ...

After killBill is called, there is nothing the Bill code can do to cause further effects.

Detailed Proposal

You can view the spec text draft in ecmarkup format or rendered as HTML.

  1. Introduce the Realm class as an officially recognized part of the ECMAScript standard API.

  2. Add to the Realm class a static method, Realm.immutableRoot(), which obtains an immutable root realm in which all primordials are already transitively immutable. These primordials include all the primordials defined as mandatory in ES2016. (And those in draft ES2017 as of March 17, 2016, the time of this writing.) These primordials must include no other objects or properties beyond those specified here. In an immutable root realm the global object itself is also transitively immutable. Specifically, it contains no host-specific objects. This frozen global object is a plain object whose [[Prototype]] is Object.prototype, i.e., the %ObjectPrototype% intrinsic of that immutable root realm.

    • Since two immutable root realms are forever the same in all ways except object identity, we leave it implementation-defined whether Realm.immutableRoot() always creates a fresh one, or always returns the same one. On any given implementation, it must either be always fresh or always the same.
  3. In order to attain the necessary deep immutability of an immutable root realm, two of its primordials must be modified from the existing standard: An immutable root realm's Date object has its now() method removed and its default constructor changed to throw a TypeError rather than reveal the current time. An immutable root realm's Math object has its random() method removed.

  4. Add to the Realm class an instance method, spawn(endowments).

    1. spawn creates a new lightweight child realm with its own fresh global object (denoted below by the symbol freshGlobal) whose [[Prototype]] is the parent realm's global object. This fresh global is also a plain object. Unlike the global of an immutable root realm, this new freshGlobal is not frozen by default.

    2. spawn populates this freshGlobal with overriding bindings for the evaluators that have global names (currently only eval and Function). It binds each of these names to fresh objects whose [[Prototype]]s are the corresponding objects from the parent realm.

    3. spawn copies the own enumerable properties from the endowments record onto the freshGlobal.

    4. spawn returns that new child realm instance.

    The total cost of a lightweight realm is four objects: the realm instance itself, the freshGlobal, and the eval function and Function constructor specific to it.

  5. The evaluators of a spawned realm evaluate code in the global scope of that realm's global, using that global as their global object.

    A lightweight realm's initial eval inherits from its parent's eval. For each of the overriding constructors (currently only Function), its prototype property initially has the same value as the constructor they inherit from. Thus, a function foo from one descendant realm passes the foo instanceof Function test using the Function constructor of another descendant of the same parent realm. Among sibling lightweight realms, instanceof on primordial types simply works.

Polyfill example

In the Punchlines section below, we explain the non-overt channel threats that motivate the removal of Date.now and Math.random. However, usually this threat is not of interest, in which case we'd rather include the full API of ES2016, since it is otherwise safe. Indeed, Caja has always provided the full functionality of Date and Math because Caja's threat model did not demand that they be denied.

The following makeColdRealm(GoodDate, goodRandom) function, given a good Date constructor and Math.random function, makes a new frozen-enough lightweight realm, that can be used as if it is an immutable root realm -- as a spawning root for making lightweight child realms. These children are separated-enough from each other, if one is not worried about non-overt channels. Unlike the lightweight realms directly descendant from an immutable root realm, children spawned from a common cold realm share a fully functional Date and Math.

function makeColdRealm(GoodDate, goodRandom) {
  const goodNow = GoodDate.now;
  const {Date: SharedDate, Math: SharedMath} = sharedRoot;
  function FreshDate(...args) {
    if (new.target) {
      if (args.length === 0) {
        args = [+goodNow()];
      }
      return Reflect.construct(SharedDate, args, new.target);
    } else {
      return String(GoodDate());
    }
  }
  FreshDate.now = () => +goodNow();
  FreshDate.prototype = SharedDate.prototype;  // so instanceof works
  FreshDate.name = SharedDate.name;
  FreshDate.__proto__ = SharedDate;

  const FreshMath = {
    __proto__: SharedMath,
    random() { return +goodRandom(); }
  };

  return def(sharedRoot.spawn({Date: FreshDate, Math: FreshMath}));
}

In addition to Date and Math, we can create abstractions to endow a fresh global with virtualized emulations of expected host-provided globals like window, document, or XMLHttpRequest. These emulations may map into the caller's own or not. Caja's Domado library uses exactly this technique to emulate most of the conventional browser and DOM APIs, by mapping the confined code's virtual DOM into the portions of the caller's "physical" DOM that the caller specifies. In this sense, the confined code is like user-mode code in an operating system, whose virtual memory accesses are mapped to physical memory by a mapping it does not see or control. Domado remaps URI space in a similar manner. By emulating the browser API, much existing browser code runs compatibly in a virtualized browser environment as configured by the caller using Domado.

Because eval, Function, and the above Date and Math observably shadow the corresponding objects from their parent realm, the spawned environment is not a fully faithful emulation of standard ECMAScript. However, these breaks in the illusion are a necessary price of avoiding identity discontinuities between lightweight realms spawned from a common parent. We have chosen these breaks carefully to be compatible with virtually all code not written specifically to test standards conformance.

Mobile code example

Map-Reduce frameworks vividly demonstrate the power of sending the code to the data, rather than the data to the code. Flexible distributed computing systems must be able to express both.

Now that Function.prototype.toString will give a reliably evaluable string that can be sent, an immutable root realm provides a safe way for the receiver to evaluate it, in order to reconstitute that function's call behavior in a safe manner. Say we have a RemotePromise constructor that makes a remote promise for an object that is elsewhere, potentially on another machine. Below, assume that the RemotePromise constructor initializes this remote promise's private instance variable #farEval to be another remote promise, for the eval method of an immutable root realm at the location (vat, worker, agent, event loop, place, ...) where this promise's fulfillment will be. If this promise rejects, then its #farEval promise likewise rejects.

class QPromise extends Promise {
  // ... API from https://github.com/kriskowal/q/wiki/API-Reference
  // All we actually use below is fcall
}

// See https://github.com/kriskowal/q-connection
class RemotePromise extends QPromise {
  ...
  // callback must be a closed function, i.e., one whose only free
  // variables are the globals defined by ES2016 and therefore present
  // on the proto-global.
  there(callback, errback = void 0) {
    const callbackSrc = Function.prototype.toString.call(callback);
    const farCallback = #farEval.fcall(callbackSrc);
    return farCallback.fcall(this).catch(errback);
  }
}

We explain there by analogy. The familiar expression Promise.resolve(p).then(callback) postpones the callback function to some future time after the promise p has been fulfilled. In like manner, the expression RemotePromise.resolve(r).there(callback) postpones and migrates the closed callback function to some future time and space, where the object that will be designated by the fulfilled remote promise r is located. Both then and there return a promise for what callback or errback will return.

This supports a federated form of the Asynchronous Partitioned Global Address Space concurrency model used by the X10 supercomputer language, integrated smoothly with our promise framework for handling asynchrony.

How Deterministic?

We do not include any form of replay within the goals of this proposal, so this "How Deterministic" section is only important because of the punchlines at the end of this section.

Given a deterministic spec, one could be sure that two computations, run on two conforming implementations, starting from the same state and fed the same inputs, will compute the same new states and outputs. The ES5 and ES2015 specs come tantalizingly close to being deterministic. ECMAScript has avoided some common but unnecessary sources of non-determinism like Java's System.identityHashCode or the enumeration order of identity hash tables. But the ECMAScript specs fail for three reasons:

  • Genuine non-determinism, such as by Math.random().
  • Unspecified but unavoidable failure, such as out-of-memory.
  • Explicit underspecification, i.e. leaving some observable behavior up to the implementation.

The explicitly non-deterministic abilities to sense the current time (via new Date() and Date.now()) or generate random numbers (via Math.random()) are disabled in an immutable root realm, and therefore by default in each realm spawned from it. New sources of non-determinism, like makeWeakRef and getStack will not be added to immutable root realms or will be similarly disabled.

The ECMAScript specs to date have never admitted the possibility of failures such as out-of-memory. In theory this means that a conforming ECMAScript implementation requires an infinite memory machine. Unfortunately, such machines are currently difficult to obtain. Since ECMAScript is an implicitly-allocating language, the out-of-memory condition could cause computation to fail at any time. If these failures are reported by unpredictably throwing a catchable exception, then defensive programming becomes impossible. This would be contrary to the goals of much ECMAScript code. Thus, any ECMAScript computation that wishes to defend its invariants, and any synchronous computation it is entangled with must, on encountering an unpredictable error, preemptively abort without running further user code.

Even if ECMAScript were otherwise deterministically replayable, these unpredictable preemptive failures would prevent it. We examine instead the weaker property of fail-stop determinism, where each replica either fails, or succeeds in a manner identical to every other non-failing replica.

Although few in number, there are specification issues that are observably left to implementations, upon which implementations may differ. Some of these may eventually be closed by future TC39 agreement, such as enumeration order if objects are modified during enumeration (TODO link). Others, like the sort algorithm used by Array.prototype.sort are less likely to be closed. However, implementation-defined is not necessarily genuine non-determinism. On a given implementation, operations which are only implementation-defined can be deterministic within the scope of that implementation. They should be fail-stop reproducible when run on the same implementation. To make use of this for replay, however, we would need to pin down what we mean by "same implementation", which seems slippery and difficult.

The punchlines

Even without pinning down the precise meaning of "implementation-defined", a computation that is limited to fail-stop implementation-defined determinism cannot read covert channels and side channels that it was not explicitly enabled to read. Nothing can practically prevent signaling on covert channels and side channels, but approximations to determinism can practically prevent confined computations from perceiving these signals.

(TODO explain the anthropic side channel and how it differs from an information-flow termination channel.)

Fail-stop implementation-defined determinism is a great boon to testing and debugging. All non-deterministic dependencies, like the allegedly current time, can be mocked and injected in a reproducible manner.

Annex B considerations

As of ES2016, the normative optionals of Annex B are safe for inclusion as normative optionals of immutable root realms. However, where Annex B states that these are normative mandatory in a web browser, there is no such requirement for immutable root realms. Even when run in a web browser, an immutable root realm, having no host specific globals, must be considered a non-browser environment. Some post-ES2015 APIs proposed for Annex B, such as the RegExp statics and the Error.prototype.stack accessor property, are not safe for inclusion in immutable root realms and must be absent.

At this time, to maximize compatibility with normal ECMAScript, we do not alter an immutable root realm's evaluators to evaluate code in strict mode by default. However, we should consider doing so. Most of the code, including legacy code, that one would wish to run under an immutable root realm is probably already compatible with strict mode. Omitting sloppy mode from immutable root realms and their spawned descendants would also make sections B.1.1, B.1.2, B.3.2, B.3.3, and B.3.4 non issues. It is unclear what an immutable root realm's evaluators should specify regarding the remaining normative optional syntax in section B.1. But the syntax accepted by these evaluators, at least in strict mode, should probably be pinned down precisely by the spec.

Some of the elements of Annex B are safe and likely mandatory in practice, independent of host environment:

  • escape and unescape
  • Object.prototype.__proto__
  • String.prototype.substr
  • The String.prototype methods defined in terms of the internal CreateHTML: anchor, big, ..., sup
  • Date.prototype.getYear and Date.prototype.setYear
  • Date.prototype.toGMTString
  • __proto__ Property Names in Object Initializers

All but the last of these have been whitelisted in Caja's SES-shim for a long time without problem. (The last bullet above is syntax and so not subject to the SES-shim whitelisting mechanism.)

Discussion

Because an immutable root realm is transitively immutable, we can safely share it between ECMAScript programs that are otherwise fully isolated. This sharing gives them access to shared objects and shared identities, but no ability to communicate with each other or to affect any state outside themselves. We can even share immutable root realms between origins and between threads, since deep immutability at the specification level should make thread safety at the implementation level straightforward.

Today, to self-host builtins by writing them in ECMAScript, one must practice safe meta programming techniques so that these builtins are properly defensive. This technique is difficult to get right, especially if such self hosting is opened to ECMAScript embedders. Instead, these builtins could be defined in a lightweight realm spawned from an immutable root realm, making defensiveness easier to achieve with higher confidence.

By the rules above, a spawned realm's Function.prototype.constructor will be the parent realm's Function constructor, i.e., identical to the spawned realm's Function.__proto__. In exchange for this odd topology, we obtain the pleasant property that instanceof works transparently between spawned realms by default -- unless overridden by a user's polyfill to the contrary.

In ES2016, the GeneratorFunction evaluator is not a named global, but rather an unnamed intrinsic. Upcoming evaluators are likely to include AsyncFunction and AsyncGeneratorFunction. These are likely to be specified as unnamed intrinsics as well. For all of these, the above name-based overriding of spawn is irrelevant and probably not needed anyway.

Because code evaluated within an immutable root realm is unable to cause any affects outside itself it is not given explicit access to, the evaluators of an immutable root realm should continue to operate even in environments in which CSP has forbidden normal evaluators. By analogy, CSP evaluator suppression does not suppress JSON.parse. There are few ways in which evaluating code in an immutable root realm is more dangerous than JSON data.

Other possible proposals, like private state and defensible const classes, are likely to aid the defensive programming that is especially powerful in the context of this proposal. But because the utility of such defensive programming support is not limited to frozen realms, they should remain independent proposals.

For each of the upcoming proposed standard APIs that are inherently not immutable and powerless:

they must be absent from an immutable root realm, or have their behavior grossly truncated into something safe. This spec will additionally need to say how they initially appear, if at all, in each individual spawned lightweight realm. In particular, we expect a pattern to emerge for creating a fresh loader instance to be the default loader of a fresh spawned realm. Once some proposed APIs are specced as being provided by import from builtin primordial modules, we will need to explain how they appear in an immutable root realm and/or the realms it spawns.

Open Questions

  • Should Realm.immutableRoot() return a new fresh frozen realm each time or should it always return the same one? Above we leave this implementation-defined for now to encourage implementations to experiment and see how efficient each can be made. If all can agree on one of these options, we should codify that rather than continue to leave this implementation-defined.

  • Although not officially a question within the jurisdiction of TC39, we should discuss whether the existing CSP "no script evaluation" settings should exempt an immutable root realm's evaluators, or whether CSP should be extended in order to express this differential prohibition.

  • Currently, if the value of eval is anything other than the original value of eval, any use of it in the form of a direct-eval expression will actually have the semantics of an indirect eval, i.e., a simple function call to the current value of eval. If an immutable root realm's builtin evaluators are not strict by default, then any user customization that replaces a spawned realm's global evaluators with strict-by-default wrappers will break their use for direct-eval. Fortunately, this seems to be addressed by the rest of the old Realms API.

  • The standard Date constructor reveals the current time either

    • when called as a constructor with no arguments, or
    • when called as a function (regardless of the arguments)

    Above we propose to censor the current time by having the proto-Date constructor throw a TypeError in those cases. Would another error type be more appropriate? Instead of throwing an Error, should new Date() produce an invalid date, equivalent to that produced by new Date(NaN)? If so, calling the Date constructor as a function should produce the corresponding string "Invalid Date". If we go in this direction, conceivably we could even have Date.now() return NaN. The advantage of removing Date.now instead is to support the feature-testing style practiced by ECMAScript programmers.

  • Of course, there is the perpetual bikeshedding of names. We are not attached to the names we present here.

Spec Text

Updating the spec text for this proposal

The source for the spec text is located in spec/index.emu and it is written in ecmarkup language.

When modifying the spec text, you should be able to build the HTML version in index.html by using the following command:

npm install
npm run build
open index.html

Alternative, you can use npm run watch.

Acknowledgements

The Compartment API proposed here derives directly from Moddable's earlier Compartment API, in the XS implementation of standalone SES. We thank in particular Patrick Soquet and Peter Hoddlie for repeated sessions of brainstorming and refinement.

Thanks to the regular attendees at the recent SES meetings, especially Bradley Farias, Michael Fig, Saleh Motaal, and Chip Morningstar.

Many thanks to E. Dean Tribble, Kevin Reid, Dave Herman, Michael Ficarra, Tom Van Cutsem, Kris Kowal, Kevin Smith, Terry Hayes, Daniel Ehrenberg, Ojan Vafai, Elliott Sprehn, and Alex Russell. Thanks to the entire Caja team (Jasvir Nagra, Ihab Awad, Mike Stay, Mike Samuel, Felix Lee, Kevin Reid, and Ben Laurie) for building a system in which all the hardest issues have already been worked out.

proposal-ses's People

Contributors

allen-munsch avatar caridy avatar darius avatar davidbruant avatar dckc avatar erights avatar fudco avatar petermetz avatar twaha-rahman avatar zaki-yama avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-ses's Issues

spawned realm's global

In the summary, when describing the steps, I see:

The spawn method makes (1) a new lightweight child realm with (2) a new global inheriting from its parent's global...

That doesn't seems right at first glance. Does this means that any global variable defined in the parent realm will be accessible in the spawned realm? e.g.:

sharedRoot.eval('function foo(value) { console.log(value); }');
const childRealm = sharedRoot.spawn({});
sharedRoot.eval('foo(1)'); // should this yield 1 or throw?

/cc @erights

Global code vs eval code

The frozen realm spec provides only the per-realm evaluators eval and Function. An immutable root realm also provides these as well as the unnamed intrinsics GeneratorFunction, AsyncFunction, AsyncGeneratorFunction, ... . The *Function evaluators do not concern us here. The eval function evaluates code as "eval code". The ECMAScript spec also provides for code to be evaluated as "global code" but it provides no standard API to do so. Only hosts provide ways to evaluate code as global code, and only in host-specific ways. In the browser, the contents of script tags are evaluated approximately as global code.

Without an evaluator that evaluates code as global code, one cannot easily build on frozen realms a good emulation of these host environments. This issue should probably be addressed by the underlying Realms API proposal and then used by frozen realms. But either way, frozen realms need some way to do this.

See https://github.com/FUDCo/proposal-frozen-realms/wiki/Differences-between-SES,-SES5,-and-ES5#top-level-declarations

Actual specification

It would be great if you could add the actual proposed changes to the ES spec here so they can be discussed in context.

Proof of security

I think that it would be interesting (and important) to include a proof that it is impossible to escape the isolated realm and cause unconfined side effects. Assuming one is ready of course given your (Mark) previous work on the topic.

Optimally, this is something people can run and scrutinize on their computers and would include all other ES2017 features in the "jail".

"Evaluator" details

This proposal, like Realms, does not seem to include any way to run code beside eval'ing a string. I'm told that an "Evaluator" proposal may provide such a capability. Where can I find more details about the Evaluator proposal?

Instead of removing non-deterministic APIs why not simply allow specifying the implementation?

As the title says, instead of removing Date.now / Math.random / String.prototype.localeCompare / etc, why not not just allow specifying host-implementations and provide a default implementation that is deterministic?

For example maybe some like:

let i = 0;
let rng = getRngSomehow();

const immutableRoot = Realm.makeImmutableRootRealm({
  hostImplementations: {
    random: () => rng.next(),
    now: () => i++,
    locale: 'fr-FR', // Affects appropriate ECMA-402 APIs within Realm
    promiseRejectionTracker: (promise, operation) => { /* Track rejections */},
  },
})

Where any non-specified implementations of host/implementation-defined features are given a secure deterministic implementation instead (e.g. Math.random() always returns 0 or something like that).

Relationship between spawned realm objects and their parent realm object

Based on the new realm API, these are some of the questions:

Question 1: getPrototypeOf

Object.getPrototypeOf(spawnedRealmObj) === parentRealmObj; // true or false?

answer: true

Question 2: constructor

Can spawned realms be created in user-land using an API other than parentRealm.spawn()?

Question 3: hooks

Can spawned realms define custom hooks for indirect eval and direct eval?

Question 4: init

Should init() method attempt to set intrinsics (from parent realm) into fresh global? or should it attempt to set intrinsics in the parent realm's global object? e.g.:

spawnedRealmObj.init();

Question 5: eval method

spawnedRealmObj.eval === parentRealmObj.eval; // based on the current realm spec, this can be true.

answer: yes, this is plausible.

the value of `this` in frozen realms

When writing the first draft for the spec, I've encountered the following line in InitializeHostDefinedRealm:

"If the host requires that the this binding in realm's global scope return an object other than the global object, let thisValue be such an object created in an implementation defined manner. Otherwise, let thisValue be undefined, indicating that realm's global this binding should be the global object."

I wonder what's the value of this for spawned realms, and for frozen realms? e.g.:

sharedRoot.spawn().eval('return this')

/cc @erights

Path to Stage 4

Stage 4

  • committee approval
  • implement in two browsers
  • significant in-the-field experience
  • merge test262 tests
  • write test262 tests

Stage 3

  • committee approval
  • spec editor signoff
  • spec reviewer signoff
  • receive implementor feedback
  • flesh out naming research

Stage 2

  • receive developer feedback
  • committee approval
  • spec text written
  • spec reviewers selected

Nomadic bind

If we think of the .then operator as like a monadic bind, we can think of the mobile-code supporting .there operator as like a nomadic bind ;).

`importSync` semantics

There's a function called importSync on a Compartment. I'm hoping to get some clarification around what it does and why it exists. I couldn't find any information on it in the repo.

Is this proposal about immutability or non-determinism? Or both?

https://github.com/tc39/proposal-ses#freezing-intrinsics-and-taming-globals says:

To make a transitively immutable root realm, we, respectively

  • Remove all non-standard properties
  • Remove Math.random
  • Remove Date.now
    . . .

Do you mean "immutable" or "non-deterministic"? If the former, then why is Date.now() included? Can the user change the externally-observable state of the Date object by calling Date.now or any other Date method? If the latter, then should the proposal be updated to use "non-deterministic" to match what you mean? If "both" then it's probably worth clearly calling this out in the proposal.

Also: are there use cases where you might want one but not the other? For example, might a test framework want to prevent non-determinism without preventing mutation? Or vice versa, e.g. a plugin framework wishing to prevent mutation but OK with non-deterministic behavior? I'm not very familiar with the use cases so this might be off-target, but I at least wanted to bring up the issue.

Related: #32

Differences from the shim

A checkbox at #17 says "add details about the polyfill". We're still working on the actual shim -- details yet to come. However, given our shim implementation strategy, here we list some necessary differences between the shim and actual frozen realms. Some of these differences are due to our wish to have shim avoid the need to parse and translate the source text. There are two reasons to avoid such parsing-and-translation:

  • It is easy to get an ECMAScript parser and translator for modern ECMAScript wrong. A mistake here should not compromise the security provided by the shim.
  • The additional performance cost of parsing and translation, slowing down the initial evaluation of every source string.

Sometimes the second cost will be less significant than the compatibility cost of living with these differences. We note below those problems that can be fixed by an untrusted parser and translator, for when we're willing to pay the second cost but not the first cost. Such a translator would be "untrusted" only in the sense that a bad translation will not compromise the shim's safety. Such a parser-translator does not need to be provided as an enhancement to the shim itself. It could be implemented by prior translation, where the frozen realm's evaluators only see the translated code.

No overriding intrinsics

As of April 30, 2016 -- the time of this writing -- frozen realms do not yet specify a means to override the bindings of intrinsics. This is postponed awaiting progress on the underlying Realms API. See also #15 . Nevertheless, some way to override the binding of intrinsics is a necessary part of the frozen realms proposal. However, it is impossible for our shim to provide this feature. The shim can override name bindings (as in the current frozen realms proposal), but not intrinsic binding.

This could be solved with an untrusted translator, but probably not at reasonable cost.

Strict only

Was going to mention that the shim will only allow strict and builtin functions -- no sloppy functions. However, if #21 is closed as expected (no sloppy functions in frozen realms, period), then this would no longer be a difference.

One can imagine a translation that tries to emulate sloppy code by translation to strict, but I doubt anyone will ever bother.

Obscure syntactic restrictions

The shim censors free names, without parsing, by a conservative scan of the source text for identifiers. As an expediency we will reject some technically correct source texts that use some rare representation options, such a backslash-u-curly escapes in identifiers. This difference is only a matter of convenience and could be repaired if there were a need.

If parsing-and-translating, as long as the parser accepts the full syntax and the translator generates code that avoids these obscure cases, then an untrusted parser-translator would solve this issue. For example, translating

"\u{20}"

to

"\u0020"

See https://github.com/google/caja/blob/master/src/com/google/caja/ses/atLeastFreeVarNames.js

Version skew

The shim will remove everything not on its whitelist, bounding the provided subset of ECMAScript to the intersection of what the shim authors have vetted and what the engine maker provides. Frozen realms as proposed would instead track the ECMAScript version they appear in. Frozen realms as they would actually be implemented would probably track the ECMAScript version that their underlying engine provides.

Introducing a parser-translator makes this problem worse, in that the syntax accepted would also be stuck at the version that parser understands.

Expression value rather than completion value

The specified eval function returns the completion value that results from evaluating the program text. Our shim's eval could only implement this correctly by parsing and translation which we wish to avoid. Instead, if the source test happens to parse as an Expression, then the shim's eval returns the value this expression evaluates to. Otherwise, it just evaluates the program and returns undefined.

Given the next problem, an untrusted parser-translator could simply insert an appropriate return statement to return the completion value. For example, it would translate

if (e) { x; } else { y; }

to

if (e) { return x; } else { return y; }

Top level return returns completion value

Closely related to the above completion value point, a frozen eval function evaluates its source code string as parsed according to the "Program" production. However the shim's eval evaluates its source as parsed according to the "FunctionBody" production. The only salient difference is that the latter admits a return statement. Thus, unlike the proposed eval function, the shim's eval will allow a top level return statement. Whatever value it returns will be treated as the (otherwise missing) completion value.

An untrusted parser-translator could parse using the Program production, rejecting sources containing a top level return.

No direct eval

The proposed eval function, if used in the direct eval syntax, does a direct eval. The shim cannot emulate this. Instead, calling the shim's eval always results in an indirect eval regardless of syntax.

This cannot be solved with an untrusted parser-translator. It is too dangerous to try to solve it with a trusted parser-translator.

typeof variable

As specified, typeof variable, for a variable name that is not in scope, will return the string "undefined". However, as specified, evaluating a variable name that is not in scope will otherwise throw a ReferenceError. The shim has no way to distinguish these two. This leaves only two reasonable choices:

  • Have the reading of a variable name that is not in scope silently return undefined, which is indistinguishable from the variable being in scope with a value of undefined.
  • Have typeof variable, for a variable name that is not in scope, throw a ReferenceError.

Since few correct programs rely on the absence of a thrown ReferenceError but many correct programs rely on typeof variable not throwing, the shim will take the first option. Prior experience with Caja corroborates this.

Given that an untrusted parser-translator generates code to bind the top-level this value to, e.g., the variable name __global__, then it could translate

typeof x

to

typeof __global__.x

In that case, we'd rather that the shim's behavior was the second bullet above, throwing a ReferenceError on all evaluations of a variable name that is not in scope. This suggests that, perhaps, we need to make the shim configurable between these two options.

See https://github.com/FUDCo/proposal-frozen-realms/wiki/Differences-between-SES,-SES5,-and-ES5#typeof-variable

this-binding of global function calls

(TODO explain)

An untrusted parser-translator can solve this by translating all simple (non-method) function call expression to ensure that they are not picking up an incidental this-binding. For example, translating

.... foo(x, y) ....

to

.... (1,foo)(x, y) ...

See https://github.com/FUDCo/proposal-frozen-realms/wiki/Differences-between-SES,-SES5,-and-ES5#this-binding-of-global-function-calls

No global code evaluator

As explained at #24 , the current frozen realms proposal only provides evaluators to evaluate code as "eval code", not "global code", but it needs some way to do the latter. Given #21 the frozen realm proposal's eval will evaluate code as "strict eval code" which is even more different than global code.

Without parsing and translation, the shim has no way to evaluate code as global code. Independent of #21, the shim can only evaluate code as strict eval code. Thus, top level global variable declarations in code evaluated by the shim will create a variable local to the top level of that evaluation, rather than creating a binding of the shim's emulation of that realm's global object. This breaks legacy patterns of using the global object for intermodule linkage, for legacy code that could otherwise run within a frozen realm without problem.

An untrusted parser-translator could solve this by translating a top level

var x = expr;
var y;

to a top level

this.x = expr;
this.y = this.y;

See https://github.com/FUDCo/proposal-frozen-realms/wiki/Differences-between-SES,-SES5,-and-ES5#top-level-declarations

Initialization cost

The biggest practical difference is not a formal difference. It is the performance cost of initializing the shim. This results in longer initial page loads, which is a fatal cost to many applications.

Adding a parser-translator to the shim itself would make this worse. OTOH, adding the parser-translator only as a development time preprocessing step only slow development, not runtime.

concerns about realm sharing self hosted intrinsics

If I understand correctly, the central idea of spawn is that realm can share immutable intrinsics and that TheFrozenRealm provides a particularly useful set of sharable intrinsics.

However, I'm concened that the proposal does not explore deeply enough the currently specified semantics of realms and intrinsic functions and that the current semantics actually invalidates assumptions made in this proposal.

The Summary section of the proposal starts out with the statement "a realm consists of a global object and an associated set of primordial objects...". That is an overly simplified models of realms. Clause 8.2 defines other state that is associated with each realm which would have to be accounted for when "spawning" a new realm. One of those fields reference a global environment record that is uniquely associated with the realm. Note that it is that global environment record that is used to resolve global references from code defined within the realm rather than the realm's global object directly.

Also note that intrinsic functions are specified to be Built In Function Objects which are permitted to be either ECMAScript function objects or an implementation provided exotic function objects. An intrinsic would be an ECMAScript function object when an implementation (or Realm creator, assuming the Realm API permits it) "self hosts" the intrinsic using ECMAScript code.

All functions that are Built In Function Objects are required to have a [[Realm]] internal slot. 9.2 defines the [[Realm]] internal slot of a function as "The realm in which the function was created and which provides any intrinsic objects that are accessed when evaluating the function. " ECMAScript function objects (including those used to implement intrinsics) also have a [[Environment]] internal slot that is "The Lexical Environment that the function was closed over.". Functions defined at the top level of a script have have their [[Environment]] set to the Global Environment Record of the realm within the creating script is running. Functions defined at the top level of a module have their [[Environment]] set to the Module Environment Record of the modules. All Module Environment Records are inner scopes of some Global Environment Record that is uniquely associated with some realm.

The implication of the above is that any intrinsics that are "accessed when evaluating a [intrinsic] function" would be resolved from the intrinsics of the realm for which the function was originally created and not from any realm with which it has been shared via spawning. Concretely a reference to %eval% would using %eval % from the original realm and never a spawned realm. Similarly any lexically scoped references (particularly to globals) from intrinsics defined using ECMAScript function objects will be resolved using the global environment record found on the function's [[Environment]] chain. This probably makes sense from a closure perspective. But it may be surprising to anybody who expects intrinsics to have access to the globals of the realm from which they were accessed.

Finally, the ES specification recently added a new internal slot [[ScriptOrModule]] to function objects (including all Built In Function objects) that references a Script Record or a Module Record. Script Record and Module Records are uniquely associated with a realm. It is apparently set to the script/module/realm of the job that created the function. [[ScriptOrModule]] is apparently used to set the active realm when the associated function is activated to start a new job (for example, if the function is used as an event handler??).

So, it appears that spawning realms with shared intrinsic function may lead to various situations where execution is associated with a realm other than the realm from which the function as accessed as an intrinsic. It is not clear whether this is expected/desirable/etc.

"strict mode" in spawned realms

I know @erights has some concerns about reducing the capabilities of the spawned realms, but I wonder if this is a decent compromise, at least for frozen realms which are already crippled by their inability to polyfill any primordial or poisoning the parent realm. It seems that preventing sloppy mode we can eliminate all vectors that could cause a new binding to be created in parent realm, forcing globals to be defined in freshGlobals, and this will certainly make it easier to spec :)

What about shimming?

Modern web practice often first loads shim code, to enhance/correct underlying browser behavior to conform more closely to some anticipated standard. However, this is impossible for the proto-SES realm. This impossibility is fundamental, since anything implicitly shared by mutually suspicious parties with no prior coordination can clearly not first be shimmed by any one of them.

The SES realms can only do name overriding of the proto-SES realm. They cannot, for example, override Array.prototype since it is also an intrinsic reachable by syntax. This creates a continued need for the SES-shim as well. Thus, we should also seek help, like the to-be-proposed "def" bulk freeze primitive, for making the SES-shim approach more affordable as well.

The Realms API may enable another way to make a SES-shim much cheaper.

[task] formalization of this proposal

  • rename this repo to proposal-frozen-realms
  • transfer repo to tc39 org
  • set up the build for spec text
  • add details about the polyfill
  • gh-pages for spec text

Use case: Freezing intrinsics after loading only the polyfill modules

Since the way to freeze intrinsics is provided as a lockdown function, it can only be done after all modules have been executed. This does not protect against the risk of malicious modules being injected.

import "./polyfill.mjs";
import { foo } from "./some-dep.mjs"; // already executed

// freeze intrinsics
lockdown();

foo();

How about introducing a new syntax instead of a function?

import "./polyfill.mjs";

// freeze intrinsics
do lockdown;

// executed later
import { foo } from "./some-dep.mjs";

foo();

Alternatively, Stage 1 Deferring Module Evaluation might help this issue.

import "./polyfill.mjs";
import { foo } from "./some-dep.mjs" with { lazyInit: true };

// freeze intrinsics
lockdown();

foo();

How does the module map deal with relative paths?

Lots of JS environments use module specifiers which could be either relative or absolute paths. In this case, the module specifier is usually converted into an absolute form, and then that absolute path is used as the key in the module map. However, the conversion/normalization is often done in a host-specific way (since different systems have different specifier syntaxes).

How does the moduleMap of Compartments enable paths to be made absolute, so they can serve as cache keys? What even is the type of the moduleMap exactly?

What about SES and WASM?

On a recent post on cap-talk I claim that WASM, by virtue of operating within the JavaScript heap and some other assumptions, would inherit the integrity/security properties of SES without need for any additional mechanism. (See that message for the full rationale.)

Need to verify this with WASM experts.

Initialization and the lazy freezing algorithm

For lazy freezing, we have been discussing the following changes.

Below we distinguish eager freezing (at initialization) and lazy freezing (on demand).

A. Overall process

Create the realm object, in this case a root realm with it own set of intrinsic.
Let the developer adds objects to the global and or alter the intrinsic.
Let the developer call a method to freeze the realm.
B. Performance optimization (suggestion)
Update the evaluator to declare updated list of frozen objects (as long as the references to those objects are frozen on the global.

C. What to eager freeze
The whole standard lib (which saves 5 ms) except "intl" (which saves 2 ms).

D. Augment the set to eager freeze (suggestion)
Between creation and freezing, any object accessed on the global should be eager-frozen.

E. The init hook vs a "freeze" method.
The current specs rely on the init hook:

11.5.1 Realm.prototype.init()
Note Extensible web: This is the dynamic way to define globals in a new realm
The init hook is invoked by the constructor. Some could see this as an anti-pattern because if something fails during the init, the developer is left without an a resource. Also, constructor should be simple, otherwise they are hard to test. They typically initialize fields, and actual work is done elsewhere, according to SRP.

The init method of the current specs can be simply achieved by a subclass:

class myRealm extends Realm {
constructor(options) {
super(options);
// do the init work here.
}
}
On the other hand, having to call a method to complete the initialization can be seen as going against the RAII principle. The developer must remember to call the init, and until then the object is in an unfinished state and should not be used.

It seems advantageous to create a fully functional realm, as opposed to a "frozen realm on standby", and have the method doing the freeing also do the conversion from realm to frozen realm.

A possible solution to resolve override mistake without break change

change behaviour for overriding non-writable property on proto will make peril for real world:

'use strict';
const obj = { __proto__: Object.freeze({ __proto__: null, a: null }) };
module.exports = (x) => {
    obj[x] = null;// assert x is not "a" but now it missed
    // continue to do danger things with x
};

but as a native feature, ses can stipulate the prototypes behaviour like proxy in pseudo-code below:

Object.prototype = new Proxy(Object.prototype, {
    __proto__: null,
    defineProperty: () => false,
});

it's still writable when read property descriptor, but can't modify actually.

then ses can prevent modify prototypes without changing override mechanism.

other hack precedent in js spec:

import * as mod from 'data:text/javascript,export let a = 1;';
Object.getPrototypeDescriptor(mod, 'a').writable;// true
mod.a = 2;// error

Should explicitly break down claims by integrity vs availability vs confidentiality.

From a recent post on cap-talk:

For each type of protection we may ask "at what granularity"? We can partition most (not all) security concerns into

  • integrity -- loosely, only authorized actions happen
  • availability -- loosely, authorized actions continue to happen
  • confidentiality -- loosely, only authorized secrets are learned.

The browser's same origin policy defends only integrity. Because code from mutually suspicious origins share an event loop, they are fully vulnerable to each other regarding availability. Among things sharing an event loop, and infinite loop by one blocks all. Finally, browsers don't claim to defend confidentiality against side channels and covert channels.

SES, operating within the browser's event loop, cannot do anything about availability and is not in a position to try. However, JavaScript in a non-browser environment like a Node server is in a position to try to defend availability to an important and practical degree. [...] we need to revise the mobile code example to explain the availability cost in making remote eval available. I was clearer about that in the earlier Concurrency Strawman section Open Vat where the permission to access that remote eval power was explicit and clear. [...]

SES makes a contribution to the less valuable side of the confidentiality issue: SES helps suppress reading non-overt channels under very restrictive conditions -- where no timing channels are granted -- but does not prevent signaling on these channels. Preventing signaling would be more valuable, but no one has any practical idea how to do that by any means.

(The text that follows on cap-talk about integrity is already reflected well in the main SES document.)

A membrane-based compartment cannot prevent "evicted" code from continuing to consume resources

After killBill is called, there is nothing the Bill code can do to cause further effects, or even to continue to occupy memory.

In the absence of promises, this full statement would be true. However, because of the universal availability of promises, Bill can just continually reschedule himself on the promise queue. Even if we were to (somehow) provide the promise library only through the membrane, rather than directly, async functions mean that the builtin promise functionality can still be reached by syntax, making it undeniable.

The first part of this claim is still true: "nothing the Bill code can do to cause further effects". This is the integrity guarantee. The invalid part "or even to continue to occupy memory" is about resource use, which is about availability. On availability, by rescheduling on the promise queue, this situation is even worse -- Bill can continue to spend cpu resources as well. Nevertheless, this is still consistent with our overall architecture: Protect integrity at object granularity. No availability defense is possible at object granularity, since any object may go into an infinite loop anyway, blocking all further progress of that agent/vat/worker/event-loop.

question for use case

Hi,

To give some context; I currently have a custom render/template engine for web components, in these templates I allow JS executions via the classic moustache templates. What I currently do is iterate over my HTML to detect these templates and extract the code into a -hopefully- mostly secure sandbox (as far as that is even possible) via the proxy + with trick. however I would like to do this initial pass of "code extraction" on the server to lessen the load on my clients. BUT, the with keyword is not allowed in use-strict code, which I am bound to because I use es6-modules, classes, etc, etc. so therefor I have lost my method of sandboxing when I wish to implement my server side "rendering".

According to the good folks from the Realms proposal would this proposal allow me to make isolated scopes inside which is only available what I inject into it? Ergo sandboxing in the strictest sense of the word.

Here's an example of what my code currently does (Which I know is bad, I want to be able to remove unsafe-eval from my CSP, which is why I'm looking into proper sandboxing).

this is my "raw" HTML

<section>
    <header>        
        <b>{{ t('data.diff.current') }}</b>
        
        <b>{{ t('data.diff.new') }}</b>
    </header>
    
    <main :for="{{ product of products }}">
        <fyn-data-product product="{{ product }}"></fyn-data-product>
    </main>
</section>

this is the same HTML after the templating went over it

<section>
    <header>
        <b>{#c4acc914-0348-4a17-897a-18b0f0d8dd90}</b>
        
        <b>{#b43300d3-9366-40b5-ade2-afce0479ac64}</b>
    </header>
    
    <main :for="{#6474986d-443e-4501-90f3-8766b898e974}"></main>
</section>

this is the map the templating made for this fragment

{
    "a199308d-7564-498a-a2e9-7307ed3f5fde": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.current');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.current')",
        original: "{{ t('data.diff.current') }}",
    },
    "f855355b-c0eb-4570-a314-d33ee4550f7d": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.new');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.new')",
        original: "{{ t('data.diff.new') }}",
    },
    "44e25a1f-2dcf-4109-8987-85a09ce72d34": {
        callable: (async function anonymous(products) {
            const sandbox = new Proxy({ Math, JSON, Date, range, products }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await products;
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [ "products" ],
        code: "product of products",
        original: "{{ product of products }}",
        directive: {
            fragment: Fragment,
            key: "product",
            name: "product",
            type: "for",
        },
    },
};

infinite loop protection

one classic way to perform a denial of service in JS user code is to evaluate while(true){}. Is it out of scope here to provide a guard against this?

Why does Math.random open non-overt channels?

If we specified that Math.random is seeded by an adequate source of entropy and that it itself is a cryptographically strong random number generator, then it would be, as far as anyone (ignorant of the encapsulated seed) can tell, perfectly non-deterministic, which, amusingly, also does not enable any side channels or covert channels.

However, because someone sometime in the distant past used Math.random in the inner loop of some benchmark, all browsers made it as fast as possible while being just good enough for most statistical purposes. There have actually been exploits caused by the ability of one caller of Math.random to infer how many times someone else called it between two calls by itself. (Does anyone have a link?)

Rather than ask browser makers to provide a strong Math.random in the SES realm, it is safer to just remove it. It is impossible to write a black box conformance test that tests whether a random number generator is producing good enough randomness. There are already distinct APIs that provide good randomness anyway, though these are currently provided only by the host environment. Rather than fix Math.random, we should move some of this work into the language standard.

In any case, even though a strong enough Math.random would not open non-overt channels, it would prevent reproducibility and so hurt SES's benefits for testing and debugging.

SES should be split into proposal and shim repositories

I have now renamed tc39/proposal-frozen-realms to tc39/proposal-ses , as discussed at tc39. We should move SES documentation from https://github.com/Agoric/SES and elsewhere to here, update or remove stale documentation here, and rename https://github.com/Agoric/SES to emphasize that what remains there is just the ses-shim. We should parallel exactly the split we just did between proposal-realms and realms-shim.

(This issue is just https://github.com/Agoric/SES/issues/105 described from the other side.)

How does Secure EcmaScript (SES) relate to other subsets of 262?

The Draft Proposal for SES (Secure EcmaScript) appears to be a description of a subset of EcmaScript (262). How does it relate to other subsets of 262, and could this be explicitly documented?

The Subsetting EcmaScript diagram appears to describe some of the ways that SES relates to 262 and other subsets. Is this diagram still accurate? Would an updated diagram like this make sense to include even non-normatively in the SES proposal?

Label: question

(Originally published at: https://tantek.com/2020/022/b1/ecmascript-ses-relate-subsets)

Identity of the evaluators

In the detailed proposal, the following step describes the creation of the evaluators:

  1. spawn populates this freshGlobal with overriding
    bindings for the evaluators that have global names (currently
    only eval and Function). It binds each of these names to
    fresh objects whose [[Prototype]]s are the corresponding
    objects from the parent realm.

The second part of it is confusing. Why is the identity of the evaluators important? Specifically:

sharedRoot.eval('function foo(value) { console.log(value); }');
const childRealm = sharedRoot.spawn({});
Object.getPrototypeOf(childRealm.eval) === sharedRoot.eval; // yield *true*?

How is this important considering that both eval methods are doing the right thing?

What about the Realms API proposal?

Attn @dherman

We explicitly postponed the Realm API proposal from ES6 in order to better understand its security properties before proceeding towards standardization. The test we all agreed to try was to layer SES on it to see how it went, and to attack the result. This was in terms of the SES-shim at the time since there was no other SES then. Now that we have a concrete and much simpler notion of what builtin SES would be like, we should revisit this soon.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.