Mirror: A maybe slightly safer-ish wrapper around eval Function constructors
at v0.1.7 7.7 kB view raw
1// Keys that'll always not be included (for Node.js) 2const ignore = { 3 sys: true, 4 wasi: true, 5 crypto: true, 6 global: true, 7 undefined: true, 8 require: true, 9 Function: true, 10 eval: true, 11 process: true, 12 module: true, 13 exports: true, 14 makeSafeGlobal: true, 15 __filename: true, 16 __dirname: true, 17 console: true, 18}; 19 20const noop = function () {} as any; 21const _freeze = Object.freeze; 22const _seal = Object.seal; 23const _keys = Object.keys; 24const _getOwnPropertyNames = Object.getOwnPropertyNames; 25const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; 26const _defineProperty = Object.defineProperty; 27const _create = Object.create; 28 29type Object = Record<string | symbol, unknown>; 30 31// Whether a key is safe to access by the Proxy 32function safeKey(target: Object, key: string | symbol): string | undefined { 33 return key !== 'constructor' && 34 key !== '__proto__' && 35 key !== 'prototype' && 36 typeof key !== 'symbol' && 37 key in target 38 ? key 39 : undefined; 40} 41 42function freeze(target: Object): Object { 43 try { _freeze(target); } catch (_error) {} 44 try { _seal(target); } catch (_error) {} 45 return target; 46} 47 48const masked = new Set(); 49 50// Wrap any given target with a masking object preventing access to prototype properties 51function mask(target: any, toplevel: boolean) { 52 if ( 53 target == null || 54 (typeof target !== 'function' && typeof target !== 'object') 55 ) { 56 // If the target isn't a function or object then skip 57 return target; 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 70 // Create a stand-in object or function 71 let standin = target; 72 if (!toplevel) { 73 standin = typeof target === 'function' 74 ? (function (this: any) { 75 if (new.target === undefined) { 76 return target.apply(this, arguments); 77 } else { 78 return new (target.bind.apply(target, arguments)); 79 } 80 }) 81 : _create(null); 82 } 83 84 // Copy all known keys over to the stand-in and recursively apply `withProxy` 85 // Prevent unsafe keys from being accessed 86 const keys = ["__proto__", "constructor"]; 87 try { 88 // Chromium already restricts access to certain globals in an 89 // iframe, this try catch block is to avoid 90 // "Failed to enumerate the properties of 'Storage': access is denied for this document" 91 keys.push(..._getOwnPropertyNames(target)); 92 } catch (_error) { 93 keys.push(..._keys(target)); 94 } 95 96 const seen = new Set(); 97 for (let i = 0; i < keys.length; i++) { 98 const key = keys[i]; 99 if (seen.has(key)) { 100 continue; 101 } else if ( 102 key !== 'prototype' && 103 (typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller')) 104 ) { 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); 123 } 124 })(), 125 }); 126 } 127 } 128 } 129 130 if (standin.prototype != null) { 131 standin.prototype = _create(null); 132 } 133 134 return freeze(standin); 135} 136 137let safeGlobal: Record<string | symbol, unknown> | void; 138let vmGlobals: Record<string | symbol, unknown> = {}; 139 140function makeSafeGlobal() { 141 if (safeGlobal) { 142 return safeGlobal; 143 } 144 145 // globalThis fallback if it's not available 146 const trueGlobal = 147 typeof globalThis === 'undefined' 148 ? new Function('return this')() 149 : globalThis; 150 151 // Get all available global names on `globalThis` and remove keys that are 152 // explicitly ignored 153 const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter( 154 key => !ignore[key] 155 ); 156 157 // When we're in the browser, we can go a step further and try to create a 158 // new JS context and globals in a separate iframe 159 vmGlobals = trueGlobal; 160 let iframe: HTMLIFrameElement | void; 161 if (typeof document !== 'undefined') { 162 try { 163 iframe = document.createElement('iframe'); 164 iframe.src = document.location.protocol; 165 // We can isolate the iframe as much as possible, but we must allow an 166 // extent of cross-site scripts 167 iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); 168 iframe.referrerPolicy = 'no-referrer'; 169 document.head.appendChild(iframe); 170 // We copy over all known globals (as seen on the original `globalThis`) 171 // from the new global we receive from the iframe 172 vmGlobals = _create(null); 173 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 174 const key = trueGlobalKeys[i]; 175 vmGlobals[key] = iframe.contentWindow![key]; 176 } 177 } catch (_error) { 178 // When we're unsuccessful we revert to the original `globalThis` 179 vmGlobals = trueGlobal; 180 } finally { 181 if (iframe) iframe.remove(); 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 } 190 } 191 192 safeGlobal = _create(null); 193 194 // The safe global is initialised by copying all values from either `globalThis` 195 // or the isolated global. They're wrapped using `withProxy` which further disallows 196 // certain key accesses 197 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 198 const key = trueGlobalKeys[i]; 199 safeGlobal[key] = mask(vmGlobals[key], true); 200 } 201 202 // We then reset all globals that are present on `globalThis` directly 203 for (const key in trueGlobal) safeGlobal[key] = undefined; 204 // We also reset all ignored keys explicitly 205 for (const key in ignore) safeGlobal[key] = undefined; 206 // It _might_ be safe to expose the Function constructor like this... who knows 207 safeGlobal!.Function = SafeFunction; 208 209 // Lastly, we also disallow certain property accesses on the safe global 210 // Wrap any given target with a Proxy preventing access to unscopables 211 return freeze(safeGlobal!); 212} 213 214interface SafeFunction { 215 new (...args: string[]): Function; 216 (...args: string[]): Function; 217} 218 219function SafeFunction(...args: string[]): Function { 220 const safeGlobal = makeSafeGlobal(); 221 const code = args.pop(); 222 223 // Retrieve Function constructor from vm globals 224 const Function = vmGlobals.Function as FunctionConstructor | void; 225 const Object = vmGlobals.Object as ObjectConstructor; 226 const createFunction = (Function || Object.constructor.constructor) as FunctionConstructor; 227 228 // We pass in our safe global and use it using `with` (ikr...) 229 // We then add a wrapper function for strict-mode and a few closing 230 // statements to prevent the code from escaping the `with` block; 231 const fn = createFunction( 232 'globalThis', 233 ...args, 234 'with (globalThis) {\n"use strict";\nreturn (function () {\n' + 235 code + 236 '\n/**/;return;}).apply(this, arguments)\n}' 237 ) as Function; 238 239 // We lastly return a wrapper function which explicitly passes our safe global 240 return function () { 241 return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any)); 242 }; 243} 244 245export { SafeFunction };