Mirror: A maybe slightly safer-ish wrapper around eval Function constructors
at main 8.5 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; 28const _slice = Array.prototype.slice; 29 30type Object = Record<string | symbol, unknown>; 31 32// Whether a key is safe to access by the Proxy 33function safeKey(target: Object, key: string | symbol): string | undefined { 34 return key !== 'constructor' && 35 key !== '__proto__' && 36 key !== 'prototype' && 37 typeof key !== 'symbol' && 38 key in target 39 ? key 40 : undefined; 41} 42 43function freeze(target: Object): Object { 44 try { _freeze(target); } catch (_error) {} 45 try { _seal(target); } catch (_error) {} 46 return target; 47} 48 49const masked = new Set(); 50 51// Wrap any given target with a masking object preventing access to prototype properties 52function mask(target: any, toplevel: boolean) { 53 if ( 54 target == null || 55 (typeof target !== 'function' && typeof target !== 'object') 56 ) { 57 // If the target isn't a function or object then skip 58 return target; 59 } 60 61 if (!('constructor' in target)) { 62 toplevel = false; 63 } 64 65 if (toplevel && masked.has(target)) { 66 return target; 67 } else if (toplevel) { 68 masked.add(target); 69 } 70 71 // Create a stand-in object or function 72 let standin = target; 73 if (!toplevel) { 74 standin = typeof target === 'function' 75 ? (function (this: any) { 76 if (new.target === undefined) { 77 return target.apply(this, arguments); 78 } else { 79 const args = _slice.call(arguments); 80 args.unshift(null); 81 return new (target.bind.apply(target, args)); 82 } 83 }) 84 : _create(null); 85 } 86 87 // Copy all known keys over to the stand-in and recursively apply `withProxy` 88 // Prevent unsafe keys from being accessed 89 const keys = ["__proto__", "constructor"]; 90 try { 91 // Chromium already restricts access to certain globals in an 92 // iframe, this try catch block is to avoid 93 // "Failed to enumerate the properties of 'Storage': access is denied for this document" 94 keys.push(..._getOwnPropertyNames(target)); 95 } catch (_error) { 96 keys.push(..._keys(target)); 97 } 98 99 const seen = new Set(); 100 for (let i = 0; i < keys.length; i++) { 101 const key = keys[i]; 102 if (seen.has(key)) { 103 continue; 104 } else if ( 105 key !== 'prototype' && 106 (typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller')) 107 ) { 108 seen.add(key); 109 const descriptor = _getOwnPropertyDescriptor(standin, key) || {}; 110 if (descriptor.configurable) { 111 _defineProperty(standin, key, { 112 enumerable: descriptor.enumerable, 113 configurable: descriptor.configurable, 114 get: (() => { 115 if (!safeKey(target, key)) { 116 return noop; 117 } if (toplevel) { 118 try { 119 const value = mask(target[key], false); 120 return () => value; 121 } catch (_error) { 122 return noop; 123 } 124 } else { 125 return () => mask(target[key], false); 126 } 127 })(), 128 }); 129 } 130 } 131 } 132 133 if (standin.prototype != null) { 134 standin.prototype = _create(null); 135 } 136 137 return toplevel ? standin : freeze(standin); 138} 139 140let safeGlobal: Record<string | symbol, unknown> | void; 141let vmGlobals: Record<string | symbol, unknown> = {}; 142 143function makeSafeGlobal() { 144 if (safeGlobal) { 145 return safeGlobal; 146 } 147 148 // globalThis fallback if it's not available 149 const trueGlobal = 150 typeof globalThis === 'undefined' 151 ? new Function('return this')() 152 : globalThis; 153 154 // Get all available global names on `globalThis` and remove keys that are 155 // explicitly ignored 156 const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter( 157 key => !ignore[key] 158 ); 159 160 // When we're in the browser, we can go a step further and try to create a 161 // new JS context and globals in a separate iframe 162 vmGlobals = trueGlobal; 163 let iframe: HTMLIFrameElement | void; 164 if (typeof document !== 'undefined') { 165 try { 166 iframe = document.createElement('iframe'); 167 iframe.src = document.location.protocol; 168 // We can isolate the iframe as much as possible, but we must allow an 169 // extent of cross-site scripts 170 iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); 171 iframe.referrerPolicy = 'no-referrer'; 172 document.head.appendChild(iframe); 173 // We copy over all known globals (as seen on the original `globalThis`) 174 // from the new global we receive from the iframe 175 vmGlobals = _create(null); 176 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 177 const key = trueGlobalKeys[i]; 178 vmGlobals[key] = iframe.contentWindow![key]; 179 } 180 } catch (_error) { 181 // When we're unsuccessful we revert to the original `globalThis` 182 vmGlobals = trueGlobal; 183 } finally { 184 if (iframe) iframe.remove(); 185 } 186 } else if (typeof require === 'function') { 187 vmGlobals = _create(null); 188 const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports; 189 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 190 const key = trueGlobalKeys[i]; 191 vmGlobals[key] = scriptGlobal[key]; 192 } 193 } 194 195 safeGlobal = _create(null); 196 197 // The safe global is initialised by copying all values from either `globalThis` 198 // or the isolated global. They're wrapped using `withProxy` which further disallows 199 // certain key accesses 200 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 201 const key = trueGlobalKeys[i]; 202 safeGlobal[key] = mask(vmGlobals[key], true); 203 } 204 205 // We then reset all globals that are present on `globalThis` directly 206 for (const key in trueGlobal) safeGlobal[key] = undefined; 207 // We also reset all ignored keys explicitly 208 for (const key in ignore) safeGlobal[key] = undefined; 209 // It _might_ be safe to expose the Function constructor like this... who knows 210 safeGlobal!.Function = SafeFunction; 211 212 // Lastly, we also disallow certain property accesses on the safe global 213 // Wrap any given target with a Proxy preventing access to unscopables 214 if (typeof Proxy === 'function') { 215 // Wrap the target in a Proxy that disallows access to some keys 216 return (safeGlobal = new Proxy(safeGlobal!, { 217 // Return a value, if it's allowed to be returned and mask this value 218 get(target, _key) { 219 const key = safeKey(target, _key); 220 return !ignore[_key] && key !== undefined ? target[key] : undefined; 221 }, 222 has(_target, _key) { 223 return true; 224 }, 225 set: noop, 226 deleteProperty: noop, 227 defineProperty: noop, 228 getOwnPropertyDescriptor: noop, 229 })); 230 } else { 231 // NOTE: Some property accesses may leak through here without the Proxy 232 return freeze(safeGlobal!); 233 } 234} 235 236interface SafeFunction { 237 new (...args: string[]): Function; 238 (...args: string[]): Function; 239} 240 241function SafeFunction(...args: string[]): Function { 242 const safeGlobal = makeSafeGlobal(); 243 const code = args.pop(); 244 245 // Retrieve Function constructor from vm globals 246 const Function = vmGlobals.Function as FunctionConstructor | void; 247 const Object = vmGlobals.Object as ObjectConstructor; 248 const createFunction = (Function || Object.constructor.constructor) as FunctionConstructor; 249 250 // We pass in our safe global and use it using `with` (ikr...) 251 // We then add a wrapper function for strict-mode and a few closing 252 // statements to prevent the code from escaping the `with` block; 253 const fn = createFunction( 254 'globalThis', 255 ...args, 256 'with (globalThis) {\n"use strict";\nreturn (function () {\n' + 257 code + 258 '\n/**/;return;}).apply(this, arguments)\n}' 259 ) as Function; 260 261 // We lastly return a wrapper function which explicitly passes our safe global 262 return function () { 263 return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any)); 264 }; 265} 266 267export { SafeFunction };