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

Configure Feed

Select the types of activity you want to include in your feed.

Mutate environment to prevent any modifications and add Node VM

+89 -48
+1
package.json
··· 57 57 "@rollup/plugin-buble": "^0.21.3", 58 58 "@rollup/plugin-commonjs": "^21.0.2", 59 59 "@rollup/plugin-node-resolve": "^13.1.3", 60 + "@types/node": "^18.0.6", 60 61 "@types/react": "^17.0.42", 61 62 "husky-v4": "^4.3.8", 62 63 "lint-staged": "^12.3.7",
+82 -47
src/index.ts
··· 18 18 }; 19 19 20 20 const noop = function () {} as any; 21 + const _freeze = Object.freeze; 22 + const _seal = Object.seal; 23 + const _keys = Object.keys; 24 + const _getOwnPropertyNames = Object.getOwnPropertyNames; 25 + const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; 26 + const _defineProperty = Object.defineProperty; 27 + const _create = Object.create; 21 28 22 29 type Object = Record<string | symbol, unknown>; 23 30 ··· 33 40 } 34 41 35 42 function freeze(target: Object): Object { 36 - return typeof Object.freeze === 'function' 37 - ? Object.freeze(target) 38 - : target; 43 + try { _freeze(target); } catch (_error) {} 44 + try { _seal(target); } catch (_error) {} 45 + return target; 39 46 } 40 47 48 + const masked = new Set(); 49 + 41 50 // Wrap any given target with a masking object preventing access to prototype properties 42 - function mask(target: any) { 51 + function mask(target: any, toplevel: boolean) { 43 52 if ( 44 53 target == null || 45 54 (typeof target !== 'function' && typeof target !== 'object') ··· 47 56 // If the target isn't a function or object then skip 48 57 return target; 49 58 } 59 + 60 + if (!('constructor' in target)) { 61 + toplevel = false; 62 + } 63 + 64 + if (toplevel && masked.has(target)) { 65 + return target; 66 + } else if (toplevel) { 67 + masked.add(target); 68 + } 69 + 50 70 // Create a stand-in object or function 51 - const standin = 52 - typeof target === 'function' 71 + let standin = target; 72 + if (!toplevel) { 73 + standin = typeof target === 'function' 53 74 ? (function (this: any) { 54 - return target.apply(this, arguments); 75 + if (new.target === undefined) { 76 + return target.apply(this, arguments); 77 + } else { 78 + return new (target.bind.apply(target, arguments)); 79 + } 55 80 }) 56 - : Object.create(null); 81 + : _create(null); 82 + } 83 + 57 84 // Copy all known keys over to the stand-in and recursively apply `withProxy` 58 85 // Prevent unsafe keys from being accessed 59 86 const keys = ["__proto__", "constructor"]; ··· 61 88 // Chromium already restricts access to certain globals in an 62 89 // iframe, this try catch block is to avoid 63 90 // "Failed to enumerate the properties of 'Storage': access is denied for this document" 64 - keys.push(...Object.getOwnPropertyNames(target)); 65 - } catch (e) {} 91 + keys.push(..._getOwnPropertyNames(target)); 92 + } catch (_error) { 93 + keys.push(..._keys(target)); 94 + } 66 95 96 + const seen = new Set(); 67 97 for (let i = 0; i < keys.length; i++) { 68 98 const key = keys[i]; 69 - if ( 99 + if (seen.has(key)) { 100 + continue; 101 + } else if ( 70 102 key !== 'prototype' && 71 103 (typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller')) 72 104 ) { 73 - Object.defineProperty(standin, key, { 74 - enumerable: true, 75 - get: safeKey(target, key) 76 - ? () => { 77 - return typeof target[key] === 'function' || 78 - typeof target[key] === 'object' 79 - ? mask(target[key]) 80 - : target[key]; 105 + seen.add(key); 106 + const descriptor = _getOwnPropertyDescriptor(standin, key) || {}; 107 + if (descriptor.configurable) { 108 + _defineProperty(standin, key, { 109 + enumerable: descriptor.enumerable, 110 + configurable: descriptor.configurable, 111 + get: (() => { 112 + if (!safeKey(target, key)) { 113 + return noop; 114 + } if (toplevel) { 115 + try { 116 + const value = mask(target[key], false); 117 + return () => value; 118 + } catch (_error) { 119 + return noop; 120 + } 121 + } else { 122 + return () => mask(target[key], false); 81 123 } 82 - : noop, 83 - }); 124 + })(), 125 + }); 126 + } 84 127 } 85 128 } 86 - if (standin.prototype != null) 87 - standin.prototype = freeze(Object.create(null)); 129 + 130 + if (standin.prototype != null) { 131 + standin.prototype = _create(null); 132 + } 133 + 88 134 return freeze(standin); 89 135 } 90 136 ··· 104 150 105 151 // Get all available global names on `globalThis` and remove keys that are 106 152 // explicitly ignored 107 - const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter( 153 + const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter( 108 154 key => !ignore[key] 109 155 ); 110 156 ··· 123 169 document.head.appendChild(iframe); 124 170 // We copy over all known globals (as seen on the original `globalThis`) 125 171 // from the new global we receive from the iframe 126 - vmGlobals = Object.create(null); 172 + vmGlobals = _create(null); 127 173 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 128 174 const key = trueGlobalKeys[i]; 129 175 vmGlobals[key] = iframe.contentWindow![key]; ··· 134 180 } finally { 135 181 if (iframe) iframe.remove(); 136 182 } 183 + } else if (typeof require === 'function') { 184 + vmGlobals = _create(null); 185 + const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports; 186 + for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 187 + const key = trueGlobalKeys[i]; 188 + vmGlobals[key] = scriptGlobal[key]; 189 + } 137 190 } 138 191 139 - safeGlobal = Object.create(null); 192 + safeGlobal = _create(null); 140 193 141 194 // The safe global is initialised by copying all values from either `globalThis` 142 195 // or the isolated global. They're wrapped using `withProxy` which further disallows 143 196 // certain key accesses 144 197 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 145 198 const key = trueGlobalKeys[i]; 146 - safeGlobal[key] = mask(vmGlobals[key]); 199 + safeGlobal[key] = mask(vmGlobals[key], true); 147 200 } 148 201 149 202 // We then reset all globals that are present on `globalThis` directly ··· 152 205 for (const key in ignore) safeGlobal[key] = undefined; 153 206 // It _might_ be safe to expose the Function constructor like this... who knows 154 207 safeGlobal!.Function = SafeFunction; 208 + 155 209 // Lastly, we also disallow certain property accesses on the safe global 156 210 // Wrap any given target with a Proxy preventing access to unscopables 157 - if (typeof Proxy === 'function') { 158 - // Wrap the target in a Proxy that disallows access to some keys 159 - return (safeGlobal = new Proxy(safeGlobal!, { 160 - // Return a value, if it's allowed to be returned and mask this value 161 - get(target, _key) { 162 - const key = safeKey(target, _key); 163 - return key !== undefined ? target[key] : undefined; 164 - }, 165 - has(_target, _key) { 166 - return true; 167 - }, 168 - set: noop, 169 - deleteProperty: noop, 170 - defineProperty: noop, 171 - getOwnPropertyDescriptor: noop, 172 - })); 173 - } else { 174 - // NOTE: Some property accesses may leak through here without the Proxy 175 - return (safeGlobal = mask(safeGlobal)); 176 - } 211 + return freeze(safeGlobal!); 177 212 } 178 213 179 214 interface SafeFunction {
+1 -1
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 - "types": [], 3 + "types": ["node"], 4 4 "baseUrl": "./", 5 5 "esModuleInterop": true, 6 6 "forceConsistentCasingInFileNames": true,
+5
yarn.lock
··· 136 136 resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.4.tgz#48aedbf35efb3af1248e4cd4d792c730290cd5d6" 137 137 integrity sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA== 138 138 139 + "@types/node@^18.0.6": 140 + version "18.0.6" 141 + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7" 142 + integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw== 143 + 139 144 "@types/parse-json@^4.0.0": 140 145 version "4.0.0" 141 146 resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"