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