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 } finally {
145 if (iframe) iframe.remove();
146 }
147 }
148
149 safeGlobal = Object.create(null);
150
151 // The safe global is initialised by copying all values from either `globalThis`
152 // or the isolated global. They're wrapped using `withProxy` which further disallows
153 // certain key accesses
154 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
155 const key = trueGlobalKeys[i];
156 safeGlobal[key] = withProxy(vmGlobals[key]);
157 }
158
159 // We then reset all globals that are present on `globalThis` directly
160 for (const key in trueGlobal) safeGlobal[key] = undefined;
161 // We also reset all ignored keys explicitly
162 for (const key in ignore) safeGlobal[key] = undefined;
163 // It _might_ be safe to expose the Function constructor like this... who knows
164 safeGlobal!.Function = SafeFunction;
165 // Lastly, we also disallow certain property accesses on the safe global
166 return (safeGlobal = withProxy(safeGlobal!));
167}
168
169interface SafeFunction {
170 new (...args: string[]): Function;
171 (...args: string[]): Function;
172}
173
174function SafeFunction(...args: string[]): Function {
175 const safeGlobal = makeSafeGlobal();
176 const code = args.pop();
177
178 // We pass in our safe global and use it using `with` (ikr...)
179 // We then add a wrapper function for strict-mode and a few closing
180 // statements to prevent the code from escaping the `with` block;
181 const fn = new Function(
182 'globalThis',
183 ...args,
184 'with (globalThis) {\n"use strict";\nreturn (function () {\n' +
185 code +
186 '\n/**/;return;}).apply(this, arguments)\n}'
187 ) as Function;
188
189 // We lastly return a wrapper function which explicitly passes our safe global
190 return function () {
191 return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any));
192 };
193}
194
195export { SafeFunction };