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