JavaScript sandbox mode

Time:2022-6-11

The micro front end has become a hot topic in the front-end field. In terms of technology, the front-end sandbox is a topic that cannot be bypassed all the time

What is a sandbox

Sandboxie (also known as sandbox, sandbox) is a virtual system program that allows you to run browsers or other programs in the sandbox environment, so the changes caused by running can be deleted later. It creates a sandbox like independent working environment, and the programs running in it cannot have a permanent impact on the hard disk. In network security, Sandbox refers to a tool used to test the behavior of untrusted files or applications in an isolated environment

In short, a sandbox is an environment isolated from the outside world. The internal and external environments do not affect each other. The outside world cannot modify any information in the environment. The things in the sandbox belong to a single world.

Sandbox for JavaScript

For JavaScript, sandbox is not a sandbox in the traditional sense. It is just a hack writing method in syntax. Sandbox is a security mechanism that runs some untrusted code in the sandbox so that it cannot access the code outside the sandbox. When it is necessary to parse or execute untrusted JavaScript and isolate the execution environment of the executed code, it is necessary to restrict the accessible objects in the executed code. Generally, the closures that handle module dependencies in JavaScript can be called sandboxes.

JavaScript sandbox implementation

We can roughly divide the implementation of sandbox into two parts:

  • Build a closure environment
  • Simulate native browser objects

Build closure environment

As we know, in JavaScript, only the global scope, function scope and block scope are available from ES6. If you want to isolate the definitions of variables, functions, etc. in a piece of code, you can only encapsulate the code into a function due to the scope control of JavaScript. You can achieve the purpose of scope isolation by using function scope. Because this method of using functions to achieve scope isolation is also required, Iife (immediate call function expression) is a design pattern called self executing anonymous functions

(function foo(){
    var a = 1;
    console.log(a);
 })();
 //Variable name cannot be accessed externally
console. Log (a) // an error is thrown: "uncaught referenceerror: A is not defined"

When a function becomes an immediately executed function expression, the variables in the expression cannot be accessed externally, and it has an independent lexical scope. It not only avoids external access to variables in Iife, but also does not pollute the global scope, making up for the defects of JavaScript in scope. It is commonly used when writing plug-ins and class libraries, such as the sandbox mode in jQuery

(function (window) {
    var jQuery = function (selector, context) {
        return new jQuery.fn.init(selector, context);
    }
    jQuery.fn = jQuery.prototype = function () {
        //Methods on the prototype, that is, methods and properties that can be shared by all jQuery objects
    }
    jQuery.fn.init.prototype = jQuery.fn;
    window. jQeury = window.$ =  jQuery; // If you need to expose some attributes or methods in the outside world, you can add these attributes and methods to the window global object
})(window);

When an Iife is assigned to a variable, instead of storing the Iife itself, it stores the results returned after the Iife is executed.

var result = (function () {
    Var name = "Zhang San";
    return name;
})();
console. log(result); //  "Zhang San"

Simulation of native browser objects

The purpose of simulating native browser objects is to prevent closure environment and operate native objects. Tampering and polluting the original environment; Before we finish simulating browser objects, we need to pay attention to several rarely used APIs.

eval

The eval function converts a string to code execution and returns one or more values

Var B = Eval ("({name:'Zhang San'})")
   console.log(b.name);

Because the code executed by Eval can access closures and global ranges, it leads to the security problem of code injection, because the code can look up along the scope chain and tamper with global variables, which we do not want

new Function

The function constructor creates a new function object. Call this constructor directly to create functions dynamically

grammar

new Function ([arg1[, arg2[, ...argN]],] functionBody)

arg1, arg2, … argNThe name of the parameter used by the function must be legally named. The parameter name is a string of valid JavaScript identifiers or a comma separated list of valid strings; For example“ ×”, “Thevalue”, or “a, B”.

functionBody
A string containing JavaScript statements that include function definitions.

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(1, 2));//3

It will also encounter security problems similar to eval and relatively minor performance problems.

var a = 1;

function sandbox() {
    var a = 2;
    return new Function('return a;'); //  A here points to 1 in the uppermost global scope
}
var f = sandbox();
console.log(f())

Unlike Eval, functions created by function can only be run in the global scope. It cannot access local closure variables. They are always created in the global environment. Therefore, at runtime, they can only access global variables and their own local variables, and cannot access variables in the scope where they are created by the function constructor; However, it can still access the global scope. New function () is a better alternative to eval (). It has excellent performance and security, but it still does not solve the problem of global access.

with

With is a keyword in JavaScript that extends the scope chain of a statement. It allows half sandbox execution. What is a half sandbox? Statement to add an object to the top of the scope chain. If there is a variable in the sandbox that does not use a namespace and has the same name as an attribute in the scope chain, the variable will point to the attribute value. If there is no property with the same name, a referenceerror will be thrown.

function sandbox(o) {
            with (o){
                //a=5; 
                c=2;
                d=3;
                console. log(a,b,c,d); //  0,1,2,3 // each variable is first considered a local variable. If the local variable has the same name as an obj object attribute, the local variable will point to the obj object attribute.
            }
            
        }
        var f = {
            a:0,
            b:1
        }
        sandbox(f);       
        console.log(f);
        console. log(c,d); //  2.3 C and D are leaked to the window object

According to its principle,withFor internal useinOperator. For each variable access within the block, it evaluates the variables under sandbox conditions. If the condition is true, it retrieves the variables from the sandbox. Otherwise, the variables are searched globally. However, the with statement enables the program to find the variable value in the specified object first. Therefore, it will be slow to find variables that are not the attributes of this object, and it is not suitable for programs with performance requirements (the JavaScript engine will perform several performance optimizations during the compilation phase. Some of these optimizations rely on static analysis according to the lexical of the code and pre-determined definition positions of all variables and functions, so as to quickly find identifiers during execution). With also causes data leakage (in non strict mode, a global variable is automatically created in the global scope)

In operator

The in operator can detect whether the left operand is a member of the right operand. Where the left operand is a string or an expression that can be converted to a string, and the right operand is an object or array.

var o = {  
        a : 1,  
        b : function() {}
    }
    console.log("a" in o);  //true
    console.log("b" in o);  //true
    console.log("c" in o);  //false
    console. log("valueOf" in o);  // Return true to inherit the prototype method of the object
    console. log("constructor" in o);  // Return true to inherit the prototype property of the object

with + new Function

With the use of with, you can slightly limit the scope of the sandbox. First, you can find the object from the current with, but if you can’t find it, you can still get it from the, polluting or tampering with the global environment.

function sandbox (src) {
    src = 'with (sandbox) {' + src + '}'
    return new Function('sandbox', src)
}
var str = 'let a = 1; window. Name= "Zhang San"; console. log(a); console. log(b)'
var b = 2
sandbox(str)({});
console. log(window.name);//' Zhang San '

Proxysandbox based on proxy implementation

Thinking from the above part, if you can use itwithFor each variable in the block, access is restricted to calculate variables under sandbox conditions, and variables are retrieved from the sandbox. So can we solve the JavaScript sandbox mechanism perfectly.

Using with and proxy to implement JavaScript sandbox

ES6 proxy is used to modify the default behavior of some operations, which is equivalent to modifying at the language level. It is a kind of “meta programming”

function sandbox(code) {
    code = 'with (sandbox) {' + code + '}'
    const fn = new Function('sandbox', code)

    return function (sandbox) {
        const sandboxProxy = new Proxy(sandbox, {
            has(target, key) {
                return true
            }
        })
        return fn(sandboxProxy)
    }
}
var a = 1;
var code = 'console.log(a)' // TypeError: Cannot read property 'log' of undefined
sandbox(code)({})

We mentioned earlierwithFor internal useinOperator. If the condition is true, it will retrieve the variable from the sandbox. There is no problem in the ideal state, but there are always some special cases, such as symbol unscopables。

Symbol.unscopables

Symbol. Symbol The unscopables property points to an object. This object specifies which attributes are excluded by the with environment when the with keyword is used.

Array.prototype[Symbol.unscopables]
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']

The above code shows that the array has 6 attributes, which will be excluded by the with command.

file

Therefore, our code needs to be modified as follows:

function sandbox(code) {
    code = 'with (sandbox) {' + code + '}'
    const fn = new Function('sandbox', code)

    return function (sandbox) {
        const sandboxProxy = new Proxy(sandbox, {
            has(target, key) {
                return true
            },
            get(target, key) {
                if (key === Symbol.unscopables) return undefined
                return target[key]
            }
        })
        return fn(sandboxProxy)
    }
}
var test = {
    a: 1,
    log(){
        console.log('11111')
    }
}
var code = 'log();console.log(a)' // 1111,TypeError: Cannot read property 'log' of undefined
sandbox(code)(test)

Symbol. Unscopables defines the non actionable properties of an object. The unscopeable property is never retrieved from the sandbox object in the with statement, but directly from the closure or global scope.

Snapshot sandbox

The following is the source code of Qiankun’s snapshotsandbox, which is partially simplified and commented here to help understand.

function iter(obj, callbackFn) {
            for (const prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    callbackFn(prop);
                }
            }
        }

        /**
         *Sandbox based on diff mode, used for low version browsers that do not support proxy
         */
        class SnapshotSandbox {
            constructor(name) {
                this.name = name;
                this.proxy = window;
                this.type = 'Snapshot';
                this.sandboxRunning = true;
                this.windowSnapshot = {};
                this.modifyPropsMap = {};
                this.active();
            }
            //Activate
            active() {
                //Record current snapshot
                this.windowSnapshot = {};
                iter(window, (prop) => {
                    this.windowSnapshot[prop] = window[prop];
                });

                //Restore previous changes
                Object.keys(this.modifyPropsMap).forEach((p) => {
                    window[p] = this.modifyPropsMap[p];
                });

                this.sandboxRunning = true;
            }
            //Restore
            inactive() {
                this.modifyPropsMap = {};

                iter(window, (prop) => {
                    if (window[prop] !== this.windowSnapshot[prop]) {
                        //Record changes and restore the environment
                        this.modifyPropsMap[prop] = window[prop];
                      
                        window[prop] = this.windowSnapshot[prop];
                    }
                });
                this.sandboxRunning = false;
            }
        }
        let sandbox = new SnapshotSandbox();
        //test
        ((window) => {
            window. Name = 'Zhang San'
            window.age = 18
            console. log(window.name, window.age) // 	 Zhang San, 18
            sandbox. inactive() // 	 reduction
            console.log(window.name, window.age) //	undefined,undefined
            sandbox. active() // 	 activation
            console. log(window.name, window.age) // 	 Zhang San, 18
        })(sandbox.proxy);

The snapshot sandbox is simple to implement. It is mainly used for low version browsers that do not support proxy. The principle is based ondiffWhen the sub application is activated or uninstalled, the sandbox is realized by recording or restoring the state in the form of snapshots. The snapshot sandbox will pollute the global window.

legacySandBox

The Qiankun framework implements proxy sandboxes in the singular mode. In order to facilitate understanding, some code is simplified and commented here.

//legacySandBox
const callableFnCacheMap = new WeakMap();

function isCallable(fn) {
  if (callableFnCacheMap.has(fn)) {
    return true;
  }
  const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
  const callable = naughtySafari ? typeof fn === 'function' && typeof fn !== 'undefined' : typeof fn ===
    'function';
  if (callable) {
    callableFnCacheMap.set(fn, callable);
  }
  return callable;
};

function isPropConfigurable(target, prop) {
  const descriptor = Object.getOwnPropertyDescriptor(target, prop);
  return descriptor ? descriptor.configurable : true;
}

function setWindowProp(prop, value, toDelete) {
  if (value === undefined && toDelete) {
    delete window[prop];
  } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
    Object.defineProperty(window, prop, {
      writable: true,
      configurable: true
    });
    window[prop] = value;
  }
}


function getTargetValue(target, value) {
  /*
    Only bind iscalable & &! isBoundedFunction && ! Isconstructable function object, such as window console、window. Atob. At present, there is no perfect detection method. Here, it is judged by whether there are enumerable extension methods in prototype
    @Warning should not be arbitrarily replaced with other judgment methods here, because some edge cases may be triggered (for example, the security exception triggered by calling the top window object in the iframe context of lodash.isfunction)
   */
  if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
    const boundValue = Function.prototype.bind.call(value, target);
    for (const key in value) {
      boundValue[key] = value[key];
    }
    if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
      Object.defineProperty(boundValue, 'prototype', {
        value: value.prototype,
        enumerable: false,
        writable: true
      });
    }

    return boundValue;
  }

  return value;
}

/**
 *Sandbox based on proxy implementation
 */
class SingularProxySandbox {
  /**Global variables added during sandbox*/
  addedPropsMapInSandbox = new Map();

  /**Global variables updated during sandbox*/
  modifiedPropsOriginalValueMapInSandbox = new Map();

  /**Continuously record the updated (newly added and modified) global variable map for snapshot at any time*/
  currentUpdatedPropsValueMap = new Map();

  name;

  proxy;

  type = 'LegacyProxy';

  sandboxRunning = true;

  latestSetProp = null;

  active() {
    if (!this.sandboxRunning) {
      this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }

    this.sandboxRunning = true;
  }

  inactive() {
    // console.log(' this.modifiedPropsOriginalValueMapInSandbox', this.modifiedPropsOriginalValueMapInSandbox)
    // console.log(' this.addedPropsMapInSandbox', this.addedPropsMapInSandbox)
    //Delete the added attribute and modify the existing attribute
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

    this.sandboxRunning = false;
  }

  constructor(name) {
    this.name = name;
    const {
      addedPropsMapInSandbox,
      modifiedPropsOriginalValueMapInSandbox,
      currentUpdatedPropsValueMap
    } = this;

    const rawWindow = window;
    //Object. Create (null) to pass in an object without a prototype chain
    const fakeWindow = Object.create(null); 

    const proxy = new Proxy(fakeWindow, {
      set: (_, p, value) => {
        if (this.sandboxRunning) {
          if (!rawWindow.hasOwnProperty(p)) {
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            //If the attribute exists in the current window object and is not recorded in the record map, the initial value of the attribute is recorded
            const originalValue = rawWindow[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }

          currentUpdatedPropsValueMap.set(p, value);
          //The window object must be reset to ensure that the updated data can be obtained in the next get
          rawWindow[p] = value;

          this.latestSetProp = p;

          return true;
        }

        //In strict mode, the proxy handler Typeerror will be thrown when set returns false. The error should be ignored when the sandbox is unloaded
        return true;
      },

      get(_, p) {
        //Avoid using window Window or window Self flees the sandbox environment and triggers to the real environment
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }
        const value = rawWindow[p];
        return getTargetValue(rawWindow, value);
      },

      Has (_, P) {// a Boolean is returned
        return p in rawWindow;
      },

      getOwnPropertyDescriptor(_, p) {
        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
        //If a property does not exist as the target object's own property, it cannot be set as non configurable
        if (descriptor && !descriptor.configurable) {
          descriptor.configurable = true;
        }
        return descriptor;
      },
    });

    this.proxy = proxy;
  }
}

let sandbox = new SingularProxySandbox();

((window) => {
  window. Name = 'Zhang San';
  window.age = 18;
  window. Sex = 'male';
  console. log(window.name, window.age,window.sex) // 	 Zhang San, 18, male
  sandbox. inactive() // 	 reduction
  console. log(window.name, window.age,window.sex) // 	 Zhang San, undefined, undefined
  sandbox. active() // 	 activation
  console. log(window.name, window.age,window.sex) // 	 Zhang San, 18, male
})(sandbox.proxy); //test

Legacysandbox can still operate the window object, but it can realize sandbox isolation by returning the atomic application state when the sandbox is activated and restoring the main application state when the sandbox is unloaded. This will also pollute the window, but its performance is better than that of the snapshot sandbox. It does not need to traverse the window object.

Proxysandbox (multiple sandbox)

In Qiankun’s Sandbox proxysandbox source code, the object fakewindow is proxied. This object is obtained through the createfakewindow method, which copies the document, location, top, window and other attributes of the window and gives them to fakewindow.

Source code display:

function createFakeWindow(global: Window) {
  // map always has the fastest performance in has check scenario
  // see https://jsperf.com/array-indexof-vs-set-has/23
  const propertiesWithGetter = new Map();
  const fakeWindow = {} as FakeWindow;

  /*
   copy the non-configurable property of global to fakeWindow
   see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
   > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
   */
  Object.getOwnPropertyNames(global)
    .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return !descriptor?.configurable;
    })
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        /*
         make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
         see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
         > The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
         */
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          descriptor.configurable = true;
          /*
           The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
           Example:
            Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
            Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
           */
          if (!hasGetter) {
            descriptor.writable = true;
          }
        }

        if (hasGetter) propertiesWithGetter.set(p, true);

        // freeze the descriptor to avoid being modified by zone.js
        // see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}

Because proxysandbox copies a fakewindow, it will not pollute the global window, and supports the simultaneous loading of multiple sub applications.
Please check the detailed source code:proxySandbox

About CSS isolation

Common are:

  • CSS Module
  • namespace
  • Dynamic StyleSheet
  • css in js
  • Shadow DOM
    We will not repeat the common ones here. Here we will focus on shadow do.

Shadow DOM

Shadow DOM allows you to attach a hidden DOM tree to a regular DOM tree — it takes the shadow root node as the starting root node. Below this root node, there can be any element, just like ordinary DOM elements.

file

This article is written by the blog one article multi posting platformOpenWriterelease!