• 周五. 12月 9th, 2022

5G编程聚合网

5G时代下一个聚合的编程学习网

热门标签

JavaScript sandbox mode

[db:作者]

1月 6, 2022

Micro front end has become a hot topic in front end field , In terms of Technology , There is a topic that can’t be bypassed by the micro front end, that is, the front end sandbox

What is a sandbox

Sandboxie( It’s also called sandbox 、 Sand table ) It’s a virtual system program , Allows you to run a browser or other program in a sandbox environment , So the changes made by the operation can be deleted later . It creates a sandbox like stand-alone environment , Programs that run inside do not have a permanent impact on the hard disk . In network security , Sandbox means in an isolated environment , Tools to test the behavior of untrusted files or applications

Simply put, sandbox (sandbox) It’s an environment isolated from the outside world , The internal and external environment does not affect each other , The outside world cannot modify any information in the environment , The things in the sandbox belong to one world alone .

JavaScript The sandbox of

about JavaScript Come on , Sandbox is not a sandbox in the traditional sense , It’s just a grammatical Hack How to write it , Sandbox is a security mechanism , Run some untrusted code in a sandbox , Make it inaccessible to code outside the sandbox . When you need to parse or execute untrusted JavaScript When , When you need to isolate the execution environment of the executed code , You need to restrict the accessible objects in the executable code , It’s usually the beginning JavaScript Closures that handle module dependencies in are called sandboxes .

JavaScript Sandbox realizes

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

  • Build a closure environment
  • Simulate native browser objects

Build closure environment

We know JavaScript in , About scope (scope), Only global scope (global scope)、 Function scope (function scope) And from ES6 Block level scopes are only available at the beginning (block scope). If you want to put a variable in a piece of code 、 The definitions of functions and so on are isolated , Limited by JavaScript Control of scope , You can only encapsulate this code into a Function in , By using function scope To achieve scope isolation . It’s also because of the need to use functions to achieve scope isolation , So there is IIFE( Call function expression now ), This one is called Design patterns for self executing anonymous functions

 (function foo(){
var a = 1;
console.log(a);
})();
// Cannot access variable from outside name
console.log(a) // Throw an error :"Uncaught ReferenceError: a is not defined"

When a function becomes an immediate function expression , Variables in expressions cannot be accessed from outside , It has a separate lexical scope . Not only does it avoid outside visits IIFE The variables in the , And it doesn’t pollute the global scope , Make up for JavaScript stay scope Defects in . It is commonly used when writing plug-ins and class libraries , Such as JQuery The sandbox mode

(function (window) {
var jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context);
}
jQuery.fn = jQuery.prototype = function () {
// The archetypal approach , That is all jQuery Objects can share methods and properties
}
jQuery.fn.init.prototype = jQuery.fn;
window.jQeury = window.$ = jQuery; // If you need to expose some properties or methods to the outside world , You can add these properties and methods to window Global objects
})(window);

When will IIFE Assign to a variable , It’s not storage IIFE In itself , It’s storage IIFE The result returned after execution .

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

Simulation of native browser objects

The purpose of emulating native browser objects is to , Prevent closure environment , Manipulate native objects . Tamper with the original environment ; Before we can simulate browser objects, we need to pay attention to a few unusual API.

eval

eval Function to convert a string to code execution , And return one or more values

 var b = eval("({name:' Zhang San '})")
console.log(b.name);

because eval The executed code has access to closures and global scopes , This leads to the security problem of code injection , Because the code can look up the scope chain , Tamper with global variables , This is what we don’t want

new Function

Function Constructor creates a new Function object . Calling this constructor directly can create functions dynamically

grammar

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

arg1, arg2, … argN The name of the parameter used by the function must be legal . Parameter name is a valid JavaScript The string of the identifier , Or a comma separated list of valid strings ; for example “×”,“theValue”, or “a,b”.

functionBody
A… That contains the definition of a function JavaScript The string of the statement .

const sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));//3

Also meet and eval Similar security issues and relatively small performance issues .

var a = 1;
function sandbox() {
var a = 2;
return new Function('return a;'); // there a Points to the 1
}
var f = sandbox();
console.log(f())

And eval The difference is Function The created function can only run in a global scope . It cannot access local closure variables , They are always created in a global environment , So at run time they can only access global variables and their own local variables , They cannot be accessed by Function The variable of the scope in which the constructor was created ; however , It can still access the global scope .new Function() yes eval() Better alternative . It has excellent performance and security , But the problem of global access has not been solved .

with

with yes JavaScript A keyword in , Extend the scope chain of a statement . It allows half sandbox execution . What’s 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 , Has the same name as an attribute in the scope chain , Then the variable will point to the property value . If there is no property with the same name , Will throw ReferenceError.

 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 and obj An attribute of an object has the same name , Then this local variable will point to obj Object properties .
}
}
var f = {
a:0,
b:1
}
sandbox(f);
console.log(f);
console.log(c,d); // 2,3 c、d Was leaked to window On the object 

Study its principle ,with Use… Internally in Operator . For each variable in the block, access , It calculates variables under sandbox conditions . If the condition is true, It will retrieve variables from the sandbox . otherwise , Just look up the variables globally . however with Statement makes the program look up the value of a variable , All of them are found in the specified object first . So for variables that are not properties of this object , It’s going to be slow to find out , Not suitable for programs with performance requirements (JavaScript The engine performs several performance optimizations during the compilation phase . Some of these optimizations rely on being able to do static analysis based on the lexical approach of the code , And predetermine the definition position of all variables and functions , In order to quickly find the identifier during execution .).with It can also lead to data leakage ( In a non-strict mode , A global variable is automatically created in the global scope )

in Operator

in Operator can detect whether the left-hand operand is a member of the right-hand one . among , The left-hand operand is a string , Or an expression that can be converted to a string , The right-hand 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, Inherit Object Prototype method of
console.log("constructor" in o); // return true, Inherit Object Prototype properties of 

with + new Function

coordination with Usage can slightly limit sandbox scope , Start with the current with Provides object lookup , But if you can’t find it, you can still get it from , Contaminate or tamper with the overall 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 '

be based on Proxy Sandbox of implementation (ProxySandbox)

Thinking from the above part , If it can be used with For each variable in the block, access is restricted to calculating variables under sandbox conditions , Retrieve variables from sandbox . So whether it can be solved perfectly JavaScript Sandbox mechanism .

Use with Plus proxy Realization JavaScript The sandbox

ES6 Proxy Used to modify the default behavior of certain operations , It’s equivalent to making changes at the language level , Belong to a kind of “ Metaprogramming ”(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 earlier with Use… Internally in Operator to evaluate variables , If the condition is true, It will retrieve variables from the sandbox . Ideally, no problem , But there are always some special cases , such as Symbol.unscopables.

Symbol.unscopables

Symbol.unscopables Object’s Symbol.unscopables attribute , Point to an object . This object specifies the use of with When a keyword , Which attributes will be with Environmental exclusion .

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 , Array has 6 Attributes , Will be with Order to exclude .

file

So 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 Define the inactive properties of an object .Unscopeable Attributes never come from with Statement in the sandbox object , Instead, it is retrieved directly from the closure or global scope .

Snapshot sandbox (SnapshotSandbox)

Here are qiankun Of snapshotSandbox Source code , Here to help understand, do some simplification and annotation .

 function iter(obj, callbackFn) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
callbackFn(prop);
}
}
}
/**
* be based on diff Sandbox realized in this way , For not supporting Proxy The lower version of browser
*/
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 the 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 , 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() // Restore
console.log(window.name, window.age) // undefined,undefined
sandbox.active() // Activate
console.log(window.name, window.age) // Zhang San ,18
})(sandbox.proxy);

Snapshot sandbox implementation is relatively simple , Mainly used for not supporting Proxy The lower version of browser , The principle is based on diff To achieve , When the subapplication is activated or unloaded, the sandbox is realized by recording or restoring the state in the form of snapshot ,snapshotSandbox It will pollute the whole situation window.

legacySandBox

qiankun frame singular In mode proxy Sandbox realizes , For the sake of understanding , Here we do some code reduction and annotation .

//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 isCallable && !isBoundedFunction && !isConstructable Function object of , Such as window.console、window.atob This kind of . There is no perfect way to detect it , Through here prototype Whether there are enumerable expansion methods to judge
@warning Don't replace it with another way of judgment , Because it could trigger some edge case( For example lodash.isFunction stay iframe Context may be due to calling top window Security exception triggered by object )
*/
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;
}
/**
* be based on Proxy Sandbox of implementation
*/
class SingularProxySandbox {
/** Global variables added during sandbox */
addedPropsMapInSandbox = new Map();
/** Global variables updated during sandbox */
modifiedPropsOriginalValueMapInSandbox = new Map();
/** Keep up-to-date records of ( New and modified ) Of global variables map, Used to do at any time snapshot */
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)
// Remove added properties , Modify existing properties
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) The way , 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 at present window Object has this property , And record map There is no record of , Then record the initial value of the attribute
const originalValue = rawWindow[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
currentUpdatedPropsValueMap.set(p, value);
// You have to reset window The object promises that next time get You can get the updated data
rawWindow[p] = value;
this.latestSetProp = p;
return true;
}
// stay strict-mode Next ,Proxy Of handler.set return false Will throw out TypeError, Errors should be ignored in the case of sandbox unloading
return true;
},
get(_, p) {
// Avoid using window.window perhaps window.self Escape from the sandbox , Trigger to the real world
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
}
const value = rawWindow[p];
return getTargetValue(rawWindow, value);
},
has(_, p) { // return boolean
return p in rawWindow;
},
getOwnPropertyDescriptor(_, p) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
// If the attribute does not exist as the target object's own attribute , You cannot make it 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() // Restore
console.log(window.name, window.age,window.sex) // Zhang San ,undefined,undefined
sandbox.active() // Activate
console.log(window.name, window.age,window.sex) // Zhang San ,18, male
})(sandbox.proxy); //test

legacySandBox Still able to operate window object , But by activating the sandbox, he still uses the state of the atom application , When unloading, restore the state of the main application to achieve sandbox isolation , The same will be true for window Cause pollution , But performance is better than snapshot sandbox , Don’t have to traverse the window object .

proxySandbox( Multiple sandboxes )

stay qiankun The sandbox of proxySandbox The source code is right fakeWindow This object is proxy , And this object is through createFakeWindow Method derived , The way to do this is to window Of document、location、top、window And so on, make a copy of it , Give to the 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<PropertyKey, boolean>();
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,
};
}

proxySandbox Because it’s a copy, it’s a copy fakeWindow, It will not pollute the whole situation window, Support multiple sub applications to load at the same time .
Detailed source code please see :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 focus on Shadow DO.

Shadow DOM

Shadow DOM Allow hidden DOM Tree attached to regular DOM In the tree —— It uses shadow root Node is the starting root node , Below this root node , It can be any element , And ordinary DOM The elements are the same .

file

This article by the blog one article many sends the platform
OpenWrite Release !

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注