···11+MIT License
22+33+Copyright (c) 2022 Phil Pluckthun <phil@kitten.sh>
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+57
README.md
···11+<div align="center">
22+ <h2 align="center" aria-label="evalish">eval<i>ish</i></h2>
33+ <p align="center"><strong>A maybe slightly safer-ish wrapper around eval Function constructors</strong></p>
44+ <p align="center"><i>Please maybe try something else first.. Please.</i></p>
55+ <br />
66+ <a href="https://npmjs.com/package/evalish">
77+ <img alt="NPM Version" src="https://img.shields.io/npm/v/evalish.svg" />
88+ </a>
99+ <a href="https://npmjs.com/package/evalish">
1010+ <img alt="License" src="https://img.shields.io/npm/l/evalish.svg" />
1111+ </a>
1212+ <a href="https://bundlephobia.com/result?p=evalish">
1313+ <img alt="Minified gzip size" src="https://img.shields.io/bundlephobia/minzip/evalish.svg?label=gzip%20size" />
1414+ </a>
1515+ <br />
1616+ <br />
1717+</div>
1818+1919+`evalish` is a small helper library that only exports a wrapper for the Function constructor: `SafeFunction`.
2020+2121+The `SafeFunction` constructor allows you to evaluate code and dynamically create a new function. In most environments,
2222+which at least don't have their CSP configured to disallow this, this will give you a fully executable function based
2323+on a string. As `Function` by default is a little safer than `eval` and runs everything in the global context,
2424+`SafeFunction` goes a step further and attempts to isolate the environment as much as possible.
2525+2626+It only does three simple things:
2727+- Isolate the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object) and uses a separate object using a `with` statement
2828+- Wraps all passed through globals, like `Array`, in a recursive proxy that disallows access to prototype-polluting propeties, such as `constructor`
2929+- In the browser: Creates an `iframe` element and uses that frame's globals instead
3030+3131+If you haven't run away screaming yet, maybe that's what you're looking for. Just a bit more safety.
3232+But really, I wrote this just for fun and I haven't written any tests yet and neither have I tested all edge cases.
3333+The export being named `SafeFunction` is really just ambitious.
3434+3535+However, if you found a way to break out of `SafeFunction` and did something to the outside JS environment, let me
3636+know and file an issue. I'm curious to see how far `evalish` would have to go to fully faux-isolate eval'ed code!
3737+3838+## Usage
3939+4040+First install `evalish` alongside `react`:
4141+4242+```sh
4343+yarn add use-editable
4444+# or
4545+npm install --save use-editable
4646+```
4747+4848+You'll then be able to import `SafeFunction` and pass it argument names and code,
4949+[just like the regular `Function` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function).
5050+5151+```js
5252+import { SafeFunction } from 'evalish';
5353+5454+new SafeFunction('a', 'b', 'return a + b')(1, 2); // returns `3`
5555+new SafeFunction('return window')(); // returns `undefined`
5656+new SafeFunction('return Array.isArray.constructor')(); // returns `undefined`
5757+```
···11+// These are marked with `Symbol.unscopables` for the Proxy
22+const unscopables = {
33+ __proto__: true,
44+ prototype: true,
55+ constructor: true,
66+};
77+88+// Keys that'll always not be included (for Node.js)
99+const ignore = {
1010+ sys: true,
1111+ wasi: true,
1212+ crypto: true,
1313+ global: true,
1414+ undefined: true,
1515+ require: true,
1616+ eval: true,
1717+ module: true,
1818+ exports: true,
1919+ __filename: true,
2020+ __dirname: true,
2121+ console: true,
2222+};
2323+2424+const noop = function () {} as any;
2525+2626+type Object = Record<string | symbol, unknown>;
2727+2828+// Whether a key is safe to access by the Proxy
2929+function safeKey(target: Object, key: string | symbol): string | undefined {
3030+ return key !== 'constructor' &&
3131+ key !== '__proto__' &&
3232+ key !== 'constructor' &&
3333+ typeof key !== 'symbol' &&
3434+ key in target
3535+ ? key
3636+ : undefined;
3737+}
3838+3939+// Wrap any given target with a Proxy preventing access to unscopables
4040+function withProxy(target: any) {
4141+ if (
4242+ target == null ||
4343+ (typeof target !== 'function' && typeof target !== 'object')
4444+ ) {
4545+ // If the target isn't a function or object then skip
4646+ return target;
4747+ } else if (
4848+ typeof Proxy === 'function' &&
4949+ typeof Symbol === 'function' &&
5050+ Symbol.unscopables
5151+ ) {
5252+ // Mark hidden keys as unscopable
5353+ target[Symbol.unscopables] = unscopables;
5454+ // Wrap the target in a Proxy that disallows access to some keys
5555+ return new Proxy(target, {
5656+ // Return a value, if it's allowed to be returned, and wrap that value in a proxy recursively
5757+ get(target, _key) {
5858+ const key = safeKey(target, _key);
5959+ return key !== undefined ? withProxy(target[key]) : undefined;
6060+ },
6161+ has(target, key) {
6262+ return !!safeKey(target, key);
6363+ },
6464+ set: noop,
6565+ deleteProperty: noop,
6666+ defineProperty: noop,
6767+ getOwnPropertyDescriptor: noop,
6868+ });
6969+ }
7070+7171+ // Create a stand-in object or function
7272+ const standin =
7373+ typeof target === 'function'
7474+ ? function (this: any) {
7575+ return target.apply(this, arguments);
7676+ }
7777+ : Object.create(null);
7878+ // Copy all known keys over to the stand-in and recursively apply `withProxy`
7979+ // Prevent unsafe keys from being accessed
8080+ const keys = ['constructor', 'prototype', '__proto__'].concat(
8181+ Object.getOwnPropertyNames(target)
8282+ );
8383+ for (let i = 0; i < keys.length; i++) {
8484+ const key = keys[i];
8585+ Object.defineProperty(standin, key, {
8686+ enumerable: true,
8787+ get: safeKey(target, key)
8888+ ? () => {
8989+ return typeof target[key] === 'function' ||
9090+ typeof target[key] === 'object'
9191+ ? withProxy(target[key])
9292+ : target[key];
9393+ }
9494+ : noop,
9595+ });
9696+ }
9797+9898+ return standin;
9999+}
100100+101101+let safeGlobal: Record<string | symbol, unknown> | void;
102102+103103+function makeSafeGlobal() {
104104+ if (safeGlobal) {
105105+ return safeGlobal;
106106+ }
107107+108108+ // globalThis fallback if it's not available
109109+ const trueGlobal =
110110+ typeof globalThis === 'undefined'
111111+ ? new Function('return this')()
112112+ : globalThis;
113113+114114+ // Get all available global names on `globalThis` and remove keys that are
115115+ // explicitly ignored
116116+ const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter(
117117+ key => !ignore[key]
118118+ );
119119+120120+ // When we're in the browser, we can go a step further and try to create a
121121+ // new JS context and globals in a separate iframe
122122+ let vmGlobals = trueGlobal;
123123+ let iframe: HTMLIFrameElement | void;
124124+ if (typeof document !== 'undefined') {
125125+ try {
126126+ iframe = document.createElement('iframe');
127127+ iframe.src = document.location.protocol;
128128+ // We can isolate the iframe as much as possible, but we must allow an
129129+ // extent of cross-site scripts
130130+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
131131+ iframe.referrerPolicy = 'no-referrer';
132132+ document.head.appendChild(iframe);
133133+ // We copy over all known globals (as seen on the original `globalThis`)
134134+ // from the new global we receive from the iframe
135135+ vmGlobals = Object.create(null);
136136+ for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
137137+ const key = trueGlobalKeys[i];
138138+ vmGlobals[key] = iframe.contentWindow![key];
139139+ }
140140+ } catch (_error) {
141141+ // When we're unsuccessful we revert to the original `globalThis`
142142+ vmGlobals = trueGlobal;
143143+ if (iframe) iframe.remove();
144144+ }
145145+ }
146146+147147+ safeGlobal = Object.create(null);
148148+149149+ // The safe global is initialised by copying all values from either `globalThis`
150150+ // or the isolated global. They're wrapped using `withProxy` which further disallows
151151+ // certain key accesses
152152+ for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
153153+ const key = trueGlobalKeys[i];
154154+ safeGlobal[key] = withProxy(vmGlobals[key]);
155155+ }
156156+157157+ // We then reset all globals that are present on `globalThis` directly
158158+ for (const key in trueGlobal) safeGlobal[key] = undefined;
159159+ // We also reset all ignored keys explicitly
160160+ for (const key in ignore) safeGlobal[key] = undefined;
161161+ // Lastly, we also disallow certain property accesses on the safe global
162162+ safeGlobal = withProxy(safeGlobal!);
163163+164164+ // We're now free to remove the iframe element, if we've used it
165165+ if (iframe) {
166166+ iframe.remove();
167167+ }
168168+169169+ return safeGlobal;
170170+}
171171+172172+interface SafeFunction {
173173+ new (...args: string[]): Function;
174174+ (...args: string[]): Function;
175175+}
176176+177177+function SafeFunction(...args: string[]): Function {
178178+ const safeGlobal = makeSafeGlobal();
179179+ const code = args.pop();
180180+181181+ // We pass in our safe global and use it using `with` (ikr...)
182182+ // We then add a wrapper function for strict-mode and a few closing
183183+ // statements to prevent the code from escaping the `with` block;
184184+ const fn = new Function(
185185+ 'globalThis',
186186+ ...args,
187187+ 'with (globalThis) {\n"use strict";\nreturn (function () {\n' +
188188+ code +
189189+ '\n/**/;return;}).apply(this, arguments)\n}'
190190+ ) as Function;
191191+192192+ // We lastly return a wrapper function which explicitly passes our safe global
193193+ return function () {
194194+ return fn.apply(safeGlobal, [safeGlobal].concat(arguments as any));
195195+ };
196196+}
197197+198198+export { SafeFunction };