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 };