Mirror: A maybe slightly safer-ish wrapper around eval Function constructors
at v0.1.1 6.0 kB view raw
1// These are marked with `Symbol.unscopables` for the Proxy 2const unscopables = { 3 __proto__: true, 4 prototype: true, 5 constructor: true, 6}; 7 8// Keys that'll always not be included (for Node.js) 9const ignore = { 10 sys: true, 11 wasi: true, 12 crypto: true, 13 global: true, 14 undefined: true, 15 require: true, 16 Function: true, 17 eval: true, 18 module: true, 19 exports: true, 20 __filename: true, 21 __dirname: true, 22 console: true, 23}; 24 25const noop = function () {} as any; 26 27type Object = Record<string | symbol, unknown>; 28 29// Whether a key is safe to access by the Proxy 30function safeKey(target: Object, key: string | symbol): string | undefined { 31 return key !== 'constructor' && 32 key !== '__proto__' && 33 key !== 'constructor' && 34 typeof key !== 'symbol' && 35 key in target 36 ? key 37 : undefined; 38} 39 40// Wrap any given target with a Proxy preventing access to unscopables 41function withProxy(target: any) { 42 if ( 43 target == null || 44 (typeof target !== 'function' && typeof target !== 'object') 45 ) { 46 // If the target isn't a function or object then skip 47 return target; 48 } else if ( 49 typeof Proxy === 'function' && 50 typeof Symbol === 'function' && 51 Symbol.unscopables 52 ) { 53 // Mark hidden keys as unscopable 54 target[Symbol.unscopables] = unscopables; 55 // Wrap the target in a Proxy that disallows access to some keys 56 return new Proxy(target, { 57 // Return a value, if it's allowed to be returned, and wrap that value in a proxy recursively 58 get(target, _key) { 59 const key = safeKey(target, _key); 60 return key !== undefined ? withProxy(target[key]) : undefined; 61 }, 62 has(target, key) { 63 return !!safeKey(target, key); 64 }, 65 set: noop, 66 deleteProperty: noop, 67 defineProperty: noop, 68 getOwnPropertyDescriptor: noop, 69 }); 70 } 71 72 // Create a stand-in object or function 73 const standin = 74 typeof target === 'function' 75 ? function (this: any) { 76 return target.apply(this, arguments); 77 } 78 : Object.create(null); 79 // Copy all known keys over to the stand-in and recursively apply `withProxy` 80 // Prevent unsafe keys from being accessed 81 const keys = ['constructor', 'prototype', '__proto__'].concat( 82 Object.getOwnPropertyNames(target) 83 ); 84 for (let i = 0; i < keys.length; i++) { 85 const key = keys[i]; 86 Object.defineProperty(standin, key, { 87 enumerable: true, 88 get: safeKey(target, key) 89 ? () => { 90 return typeof target[key] === 'function' || 91 typeof target[key] === 'object' 92 ? withProxy(target[key]) 93 : target[key]; 94 } 95 : noop, 96 }); 97 } 98 99 return standin; 100} 101 102let safeGlobal: Record<string | symbol, unknown> | void; 103 104function makeSafeGlobal() { 105 if (safeGlobal) { 106 return safeGlobal; 107 } 108 109 // globalThis fallback if it's not available 110 const trueGlobal = 111 typeof globalThis === 'undefined' 112 ? new Function('return this')() 113 : globalThis; 114 115 // Get all available global names on `globalThis` and remove keys that are 116 // explicitly ignored 117 const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter( 118 key => !ignore[key] 119 ); 120 121 // When we're in the browser, we can go a step further and try to create a 122 // new JS context and globals in a separate iframe 123 let vmGlobals = trueGlobal; 124 let iframe: HTMLIFrameElement | void; 125 if (typeof document !== 'undefined') { 126 try { 127 iframe = document.createElement('iframe'); 128 iframe.src = document.location.protocol; 129 // We can isolate the iframe as much as possible, but we must allow an 130 // extent of cross-site scripts 131 iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); 132 iframe.referrerPolicy = 'no-referrer'; 133 document.head.appendChild(iframe); 134 // We copy over all known globals (as seen on the original `globalThis`) 135 // from the new global we receive from the iframe 136 vmGlobals = Object.create(null); 137 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 138 const key = trueGlobalKeys[i]; 139 vmGlobals[key] = iframe.contentWindow![key]; 140 } 141 } catch (_error) { 142 // When we're unsuccessful we revert to the original `globalThis` 143 vmGlobals = trueGlobal; 144 if (iframe) iframe.remove(); 145 } 146 } 147 148 safeGlobal = Object.create(null); 149 150 // The safe global is initialised by copying all values from either `globalThis` 151 // or the isolated global. They're wrapped using `withProxy` which further disallows 152 // certain key accesses 153 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) { 154 const key = trueGlobalKeys[i]; 155 safeGlobal[key] = withProxy(vmGlobals[key]); 156 } 157 158 // We then reset all globals that are present on `globalThis` directly 159 for (const key in trueGlobal) safeGlobal[key] = undefined; 160 // We also reset all ignored keys explicitly 161 for (const key in ignore) safeGlobal[key] = undefined; 162 // Lastly, we also disallow certain property accesses on the safe global 163 safeGlobal = withProxy(safeGlobal!); 164 165 // We're now free to remove the iframe element, if we've used it 166 if (iframe) { 167 iframe.remove(); 168 } 169 170 return safeGlobal; 171} 172 173interface SafeFunction { 174 new (...args: string[]): Function; 175 (...args: string[]): Function; 176} 177 178function SafeFunction(...args: string[]): Function { 179 const safeGlobal = makeSafeGlobal(); 180 const code = args.pop(); 181 182 // We pass in our safe global and use it using `with` (ikr...) 183 // We then add a wrapper function for strict-mode and a few closing 184 // statements to prevent the code from escaping the `with` block; 185 const fn = new Function( 186 'globalThis', 187 ...args, 188 'with (globalThis) {\n"use strict";\nreturn (function () {\n' + 189 code + 190 '\n/**/;return;}).apply(this, arguments)\n}' 191 ) as Function; 192 193 // We lastly return a wrapper function which explicitly passes our safe global 194 return function () { 195 return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any)); 196 }; 197} 198 199export { SafeFunction };