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