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