Mirror: A maybe slightly safer-ish wrapper around eval Function constructors

Mutate environment to prevent any modifications and add Node VM

+1
package.json
···
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
+
"@types/node": "^18.0.6",
"@types/react": "^17.0.42",
"husky-v4": "^4.3.8",
"lint-staged": "^12.3.7",
+82 -47
src/index.ts
···
};
const noop = function () {} as any;
+
const _freeze = Object.freeze;
+
const _seal = Object.seal;
+
const _keys = Object.keys;
+
const _getOwnPropertyNames = Object.getOwnPropertyNames;
+
const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+
const _defineProperty = Object.defineProperty;
+
const _create = Object.create;
type Object = Record<string | symbol, unknown>;
···
}
function freeze(target: Object): Object {
-
return typeof Object.freeze === 'function'
-
? Object.freeze(target)
-
: target;
+
try { _freeze(target); } catch (_error) {}
+
try { _seal(target); } catch (_error) {}
+
return target;
}
+
const masked = new Set();
+
// Wrap any given target with a masking object preventing access to prototype properties
-
function mask(target: any) {
+
function mask(target: any, toplevel: boolean) {
if (
target == null ||
(typeof target !== 'function' && typeof target !== 'object')
···
// If the target isn't a function or object then skip
return target;
}
+
+
if (!('constructor' in target)) {
+
toplevel = false;
+
}
+
+
if (toplevel && masked.has(target)) {
+
return target;
+
} else if (toplevel) {
+
masked.add(target);
+
}
+
// Create a stand-in object or function
-
const standin =
-
typeof target === 'function'
+
let standin = target;
+
if (!toplevel) {
+
standin = typeof target === 'function'
? (function (this: any) {
-
return target.apply(this, arguments);
+
if (new.target === undefined) {
+
return target.apply(this, arguments);
+
} else {
+
return new (target.bind.apply(target, arguments));
+
}
})
-
: Object.create(null);
+
: _create(null);
+
}
+
// Copy all known keys over to the stand-in and recursively apply `withProxy`
// Prevent unsafe keys from being accessed
const keys = ["__proto__", "constructor"];
···
// Chromium already restricts access to certain globals in an
// iframe, this try catch block is to avoid
// "Failed to enumerate the properties of 'Storage': access is denied for this document"
-
keys.push(...Object.getOwnPropertyNames(target));
-
} catch (e) {}
+
keys.push(..._getOwnPropertyNames(target));
+
} catch (_error) {
+
keys.push(..._keys(target));
+
}
+
const seen = new Set();
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
-
if (
+
if (seen.has(key)) {
+
continue;
+
} else if (
key !== 'prototype' &&
(typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller'))
) {
-
Object.defineProperty(standin, key, {
-
enumerable: true,
-
get: safeKey(target, key)
-
? () => {
-
return typeof target[key] === 'function' ||
-
typeof target[key] === 'object'
-
? mask(target[key])
-
: target[key];
+
seen.add(key);
+
const descriptor = _getOwnPropertyDescriptor(standin, key) || {};
+
if (descriptor.configurable) {
+
_defineProperty(standin, key, {
+
enumerable: descriptor.enumerable,
+
configurable: descriptor.configurable,
+
get: (() => {
+
if (!safeKey(target, key)) {
+
return noop;
+
} if (toplevel) {
+
try {
+
const value = mask(target[key], false);
+
return () => value;
+
} catch (_error) {
+
return noop;
+
}
+
} else {
+
return () => mask(target[key], false);
}
-
: noop,
-
});
+
})(),
+
});
+
}
}
}
-
if (standin.prototype != null)
-
standin.prototype = freeze(Object.create(null));
+
+
if (standin.prototype != null) {
+
standin.prototype = _create(null);
+
}
+
return freeze(standin);
}
···
// Get all available global names on `globalThis` and remove keys that are
// explicitly ignored
-
const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter(
+
const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter(
key => !ignore[key]
);
···
document.head.appendChild(iframe);
// We copy over all known globals (as seen on the original `globalThis`)
// from the new global we receive from the iframe
-
vmGlobals = Object.create(null);
+
vmGlobals = _create(null);
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
const key = trueGlobalKeys[i];
vmGlobals[key] = iframe.contentWindow![key];
···
} finally {
if (iframe) iframe.remove();
}
+
} else if (typeof require === 'function') {
+
vmGlobals = _create(null);
+
const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports;
+
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
+
const key = trueGlobalKeys[i];
+
vmGlobals[key] = scriptGlobal[key];
+
}
}
-
safeGlobal = Object.create(null);
+
safeGlobal = _create(null);
// The safe global is initialised by copying all values from either `globalThis`
// or the isolated global. They're wrapped using `withProxy` which further disallows
// certain key accesses
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
const key = trueGlobalKeys[i];
-
safeGlobal[key] = mask(vmGlobals[key]);
+
safeGlobal[key] = mask(vmGlobals[key], true);
}
// We then reset all globals that are present on `globalThis` directly
···
for (const key in ignore) safeGlobal[key] = undefined;
// It _might_ be safe to expose the Function constructor like this... who knows
safeGlobal!.Function = SafeFunction;
+
// Lastly, we also disallow certain property accesses on the safe global
// Wrap any given target with a Proxy preventing access to unscopables
-
if (typeof Proxy === 'function') {
-
// Wrap the target in a Proxy that disallows access to some keys
-
return (safeGlobal = new Proxy(safeGlobal!, {
-
// Return a value, if it's allowed to be returned and mask this value
-
get(target, _key) {
-
const key = safeKey(target, _key);
-
return key !== undefined ? target[key] : undefined;
-
},
-
has(_target, _key) {
-
return true;
-
},
-
set: noop,
-
deleteProperty: noop,
-
defineProperty: noop,
-
getOwnPropertyDescriptor: noop,
-
}));
-
} else {
-
// NOTE: Some property accesses may leak through here without the Proxy
-
return (safeGlobal = mask(safeGlobal));
-
}
+
return freeze(safeGlobal!);
}
interface SafeFunction {
+1 -1
tsconfig.json
···
{
"compilerOptions": {
-
"types": [],
+
"types": ["node"],
"baseUrl": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
+5
yarn.lock
···
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.4.tgz#48aedbf35efb3af1248e4cd4d792c730290cd5d6"
integrity sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==
+
"@types/node@^18.0.6":
+
version "18.0.6"
+
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7"
+
integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"