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