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