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 process: true,
12 module: true,
13 exports: true,
14 makeSafeGlobal: true,
15 __filename: true,
16 __dirname: true,
17 console: true,
18};
19
20const noop = function () {} as any;
21const _freeze = Object.freeze;
22const _seal = Object.seal;
23const _keys = Object.keys;
24const _getOwnPropertyNames = Object.getOwnPropertyNames;
25const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
26const _defineProperty = Object.defineProperty;
27const _create = Object.create;
28const _slice = Array.prototype.slice;
29
30type Object = Record<string | symbol, unknown>;
31
32// Whether a key is safe to access by the Proxy
33function safeKey(target: Object, key: string | symbol): string | undefined {
34 return key !== 'constructor' &&
35 key !== '__proto__' &&
36 key !== 'prototype' &&
37 typeof key !== 'symbol' &&
38 key in target
39 ? key
40 : undefined;
41}
42
43function freeze(target: Object): Object {
44 try { _freeze(target); } catch (_error) {}
45 try { _seal(target); } catch (_error) {}
46 return target;
47}
48
49const masked = new Set();
50
51// Wrap any given target with a masking object preventing access to prototype properties
52function mask(target: any, toplevel: boolean) {
53 if (
54 target == null ||
55 (typeof target !== 'function' && typeof target !== 'object')
56 ) {
57 // If the target isn't a function or object then skip
58 return target;
59 }
60
61 if (!('constructor' in target)) {
62 toplevel = false;
63 }
64
65 if (toplevel && masked.has(target)) {
66 return target;
67 } else if (toplevel) {
68 masked.add(target);
69 }
70
71 // Create a stand-in object or function
72 let standin = target;
73 if (!toplevel) {
74 standin = typeof target === 'function'
75 ? (function (this: any) {
76 if (new.target === undefined) {
77 return target.apply(this, arguments);
78 } else {
79 const args = _slice.call(arguments);
80 args.unshift(null);
81 return new (target.bind.apply(target, args));
82 }
83 })
84 : _create(null);
85 }
86
87 // Copy all known keys over to the stand-in and recursively apply `withProxy`
88 // Prevent unsafe keys from being accessed
89 const keys = ["__proto__", "constructor"];
90 try {
91 // Chromium already restricts access to certain globals in an
92 // iframe, this try catch block is to avoid
93 // "Failed to enumerate the properties of 'Storage': access is denied for this document"
94 keys.push(..._getOwnPropertyNames(target));
95 } catch (_error) {
96 keys.push(..._keys(target));
97 }
98
99 const seen = new Set();
100 for (let i = 0; i < keys.length; i++) {
101 const key = keys[i];
102 if (seen.has(key)) {
103 continue;
104 } else if (
105 key !== 'prototype' &&
106 (typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller'))
107 ) {
108 seen.add(key);
109 const descriptor = _getOwnPropertyDescriptor(standin, key) || {};
110 if (descriptor.configurable) {
111 _defineProperty(standin, key, {
112 enumerable: descriptor.enumerable,
113 configurable: descriptor.configurable,
114 get: (() => {
115 if (!safeKey(target, key)) {
116 return noop;
117 } if (toplevel) {
118 try {
119 const value = mask(target[key], false);
120 return () => value;
121 } catch (_error) {
122 return noop;
123 }
124 } else {
125 return () => mask(target[key], false);
126 }
127 })(),
128 });
129 }
130 }
131 }
132
133 if (standin.prototype != null) {
134 standin.prototype = _create(null);
135 }
136
137 return toplevel ? standin : freeze(standin);
138}
139
140let safeGlobal: Record<string | symbol, unknown> | void;
141let vmGlobals: Record<string | symbol, unknown> = {};
142
143function makeSafeGlobal() {
144 if (safeGlobal) {
145 return safeGlobal;
146 }
147
148 // globalThis fallback if it's not available
149 const trueGlobal =
150 typeof globalThis === 'undefined'
151 ? new Function('return this')()
152 : globalThis;
153
154 // Get all available global names on `globalThis` and remove keys that are
155 // explicitly ignored
156 const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter(
157 key => !ignore[key]
158 );
159
160 // When we're in the browser, we can go a step further and try to create a
161 // new JS context and globals in a separate iframe
162 vmGlobals = trueGlobal;
163 let iframe: HTMLIFrameElement | void;
164 if (typeof document !== 'undefined') {
165 try {
166 iframe = document.createElement('iframe');
167 iframe.src = document.location.protocol;
168 // We can isolate the iframe as much as possible, but we must allow an
169 // extent of cross-site scripts
170 iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
171 iframe.referrerPolicy = 'no-referrer';
172 document.head.appendChild(iframe);
173 // We copy over all known globals (as seen on the original `globalThis`)
174 // from the new global we receive from the iframe
175 vmGlobals = _create(null);
176 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
177 const key = trueGlobalKeys[i];
178 vmGlobals[key] = iframe.contentWindow![key];
179 }
180 } catch (_error) {
181 // When we're unsuccessful we revert to the original `globalThis`
182 vmGlobals = trueGlobal;
183 } finally {
184 if (iframe) iframe.remove();
185 }
186 } else if (typeof require === 'function') {
187 vmGlobals = _create(null);
188 const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports;
189 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
190 const key = trueGlobalKeys[i];
191 vmGlobals[key] = scriptGlobal[key];
192 }
193 }
194
195 safeGlobal = _create(null);
196
197 // The safe global is initialised by copying all values from either `globalThis`
198 // or the isolated global. They're wrapped using `withProxy` which further disallows
199 // certain key accesses
200 for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
201 const key = trueGlobalKeys[i];
202 safeGlobal[key] = mask(vmGlobals[key], true);
203 }
204
205 // We then reset all globals that are present on `globalThis` directly
206 for (const key in trueGlobal) safeGlobal[key] = undefined;
207 // We also reset all ignored keys explicitly
208 for (const key in ignore) safeGlobal[key] = undefined;
209 // It _might_ be safe to expose the Function constructor like this... who knows
210 safeGlobal!.Function = SafeFunction;
211
212 // Lastly, we also disallow certain property accesses on the safe global
213 // Wrap any given target with a Proxy preventing access to unscopables
214 if (typeof Proxy === 'function') {
215 // Wrap the target in a Proxy that disallows access to some keys
216 return (safeGlobal = new Proxy(safeGlobal!, {
217 // Return a value, if it's allowed to be returned and mask this value
218 get(target, _key) {
219 const key = safeKey(target, _key);
220 return !ignore[_key] && key !== undefined ? target[key] : undefined;
221 },
222 has(_target, _key) {
223 return true;
224 },
225 set: noop,
226 deleteProperty: noop,
227 defineProperty: noop,
228 getOwnPropertyDescriptor: noop,
229 }));
230 } else {
231 // NOTE: Some property accesses may leak through here without the Proxy
232 return freeze(safeGlobal!);
233 }
234}
235
236interface SafeFunction {
237 new (...args: string[]): Function;
238 (...args: string[]): Function;
239}
240
241function SafeFunction(...args: string[]): Function {
242 const safeGlobal = makeSafeGlobal();
243 const code = args.pop();
244
245 // Retrieve Function constructor from vm globals
246 const Function = vmGlobals.Function as FunctionConstructor | void;
247 const Object = vmGlobals.Object as ObjectConstructor;
248 const createFunction = (Function || Object.constructor.constructor) as FunctionConstructor;
249
250 // We pass in our safe global and use it using `with` (ikr...)
251 // We then add a wrapper function for strict-mode and a few closing
252 // statements to prevent the code from escaping the `with` block;
253 const fn = createFunction(
254 'globalThis',
255 ...args,
256 'with (globalThis) {\n"use strict";\nreturn (function () {\n' +
257 code +
258 '\n/**/;return;}).apply(this, arguments)\n}'
259 ) as Function;
260
261 // We lastly return a wrapper function which explicitly passes our safe global
262 return function () {
263 return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any));
264 };
265}
266
267export { SafeFunction };