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