+26
.github/workflows/mirror.yml
+26
.github/workflows/mirror.yml
···
1
+
# Mirrors to https://tangled.sh/@kitten.sh (knot.kitten.sh)
2
+
name: Mirror (Git Backup)
3
+
on:
4
+
push:
5
+
branches:
6
+
- main
7
+
jobs:
8
+
mirror:
9
+
runs-on: ubuntu-latest
10
+
steps:
11
+
- name: Checkout repository
12
+
uses: actions/checkout@v4
13
+
with:
14
+
fetch-depth: 0
15
+
fetch-tags: true
16
+
- name: Mirror
17
+
env:
18
+
MIRROR_SSH_KEY: ${{ secrets.MIRROR_SSH_KEY }}
19
+
GIT_SSH_COMMAND: 'ssh -o StrictHostKeyChecking=yes'
20
+
run: |
21
+
mkdir -p ~/.ssh
22
+
echo "$MIRROR_SSH_KEY" > ~/.ssh/id_rsa
23
+
chmod 600 ~/.ssh/id_rsa
24
+
ssh-keyscan -H knot.kitten.sh >> ~/.ssh/known_hosts
25
+
git remote add mirror "git@knot.kitten.sh:kitten.sh/${GITHUB_REPOSITORY#*/}"
26
+
git push --mirror mirror
+7
-6
README.md
+7
-6
README.md
···
25
25
26
26
It only does three simple things:
27
27
- Isolate the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object) and uses a separate object using a `with` statement
28
-
- Wraps all passed through globals, like `Array`, in a recursive proxy that disallows access to prototype-polluting propeties, such as `constructor`
29
-
- In the browser: Creates an `iframe` element and uses that frame's globals instead
28
+
- Wraps all passed through globals, like `Array`, in a recursive masking object that disallows access to object prototype properties
29
+
- In the browser: Creates an `iframe` element and uses that frame's globals instead to prvent prototype pollution.
30
30
31
31
If you haven't run away screaming yet, maybe that's what you're looking for. Just a bit more safety.
32
32
But really, I wrote this just for fun and I haven't written any tests yet and neither have I tested all edge cases.
33
33
The export being named `SafeFunction` is really just ambitious.
34
34
35
-
However, if you found a way to break out of `SafeFunction` and did something to the outside JS environment, let me
36
-
know and file an issue. I'm curious to see how far `evalish` would have to go to fully faux-isolate eval'ed code!
35
+
[**However, if you found a way to break out of `SafeFunction` and did something to the outside JS environment, let me
36
+
know and file an issue.**](https://github.com/kitten/evalish/issues/new)
37
+
I'm curious to see how far `evalish` would have to go to fully faux-isolate eval'ed code!
37
38
38
39
## Usage
39
40
40
41
First install `evalish` alongside `react`:
41
42
42
43
```sh
43
-
yarn add use-editable
44
+
yarn add evalish
44
45
# or
45
-
npm install --save use-editable
46
+
npm install --save evalish
46
47
```
47
48
48
49
You'll then be able to import `SafeFunction` and pass it argument names and code,
+2
-1
package.json
+2
-1
package.json
···
2
2
"name": "evalish",
3
3
"description": "A maybe slightly safer-ish wrapper around eval Function constructors",
4
4
"DISCLAIMER": "Please maybe try something else first.. Please.",
5
-
"version": "0.1.1",
5
+
"version": "0.1.8",
6
6
"main": "dist/evalish.js",
7
7
"module": "dist/evalish.mjs",
8
8
"types": "dist/types/index.d.ts",
···
57
57
"@rollup/plugin-buble": "^0.21.3",
58
58
"@rollup/plugin-commonjs": "^21.0.2",
59
59
"@rollup/plugin-node-resolve": "^13.1.3",
60
+
"@types/node": "^18.0.6",
60
61
"@types/react": "^17.0.42",
61
62
"husky-v4": "^4.3.8",
62
63
"lint-staged": "^12.3.7",
+134
-66
src/index.ts
+134
-66
src/index.ts
···
1
-
// These are marked with `Symbol.unscopables` for the Proxy
2
-
const unscopables = {
3
-
__proto__: true,
4
-
prototype: true,
5
-
constructor: true,
6
-
};
7
-
8
1
// Keys that'll always not be included (for Node.js)
9
2
const ignore = {
10
3
sys: true,
···
15
8
require: true,
16
9
Function: true,
17
10
eval: true,
11
+
process: true,
18
12
module: true,
19
13
exports: true,
14
+
makeSafeGlobal: true,
20
15
__filename: true,
21
16
__dirname: true,
22
17
console: true,
23
18
};
24
19
25
20
const noop = function () {} as any;
21
+
const _freeze = Object.freeze;
22
+
const _seal = Object.seal;
23
+
const _keys = Object.keys;
24
+
const _getOwnPropertyNames = Object.getOwnPropertyNames;
25
+
const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
26
+
const _defineProperty = Object.defineProperty;
27
+
const _create = Object.create;
28
+
const _slice = Array.prototype.slice;
26
29
27
30
type Object = Record<string | symbol, unknown>;
28
31
···
30
33
function safeKey(target: Object, key: string | symbol): string | undefined {
31
34
return key !== 'constructor' &&
32
35
key !== '__proto__' &&
33
-
key !== 'constructor' &&
36
+
key !== 'prototype' &&
34
37
typeof key !== 'symbol' &&
35
38
key in target
36
39
? key
37
40
: undefined;
38
41
}
39
42
40
-
// Wrap any given target with a Proxy preventing access to unscopables
41
-
function withProxy(target: any) {
43
+
function freeze(target: Object): Object {
44
+
try { _freeze(target); } catch (_error) {}
45
+
try { _seal(target); } catch (_error) {}
46
+
return target;
47
+
}
48
+
49
+
const masked = new Set();
50
+
51
+
// Wrap any given target with a masking object preventing access to prototype properties
52
+
function mask(target: any, toplevel: boolean) {
42
53
if (
43
54
target == null ||
44
55
(typeof target !== 'function' && typeof target !== 'object')
45
56
) {
46
57
// If the target isn't a function or object then skip
47
58
return target;
48
-
} else if (
49
-
typeof Proxy === 'function' &&
50
-
typeof Symbol === 'function' &&
51
-
Symbol.unscopables
52
-
) {
53
-
// Mark hidden keys as unscopable
54
-
target[Symbol.unscopables] = unscopables;
55
-
// Wrap the target in a Proxy that disallows access to some keys
56
-
return new Proxy(target, {
57
-
// Return a value, if it's allowed to be returned, and wrap that value in a proxy recursively
58
-
get(target, _key) {
59
-
const key = safeKey(target, _key);
60
-
return key !== undefined ? withProxy(target[key]) : undefined;
61
-
},
62
-
has(target, key) {
63
-
return !!safeKey(target, key);
64
-
},
65
-
set: noop,
66
-
deleteProperty: noop,
67
-
defineProperty: noop,
68
-
getOwnPropertyDescriptor: noop,
69
-
});
59
+
}
60
+
61
+
if (!('constructor' in target)) {
62
+
toplevel = false;
63
+
}
64
+
65
+
if (toplevel && masked.has(target)) {
66
+
return target;
67
+
} else if (toplevel) {
68
+
masked.add(target);
70
69
}
71
70
72
71
// Create a stand-in object or function
73
-
const standin =
74
-
typeof target === 'function'
75
-
? function (this: any) {
76
-
return target.apply(this, arguments);
77
-
}
78
-
: Object.create(null);
72
+
let standin = target;
73
+
if (!toplevel) {
74
+
standin = typeof target === 'function'
75
+
? (function (this: any) {
76
+
if (new.target === undefined) {
77
+
return target.apply(this, arguments);
78
+
} else {
79
+
const args = _slice.call(arguments);
80
+
args.unshift(null);
81
+
return new (target.bind.apply(target, args));
82
+
}
83
+
})
84
+
: _create(null);
85
+
}
86
+
79
87
// Copy all known keys over to the stand-in and recursively apply `withProxy`
80
88
// Prevent unsafe keys from being accessed
81
-
const keys = ['constructor', 'prototype', '__proto__'].concat(
82
-
Object.getOwnPropertyNames(target)
83
-
);
89
+
const keys = ["__proto__", "constructor"];
90
+
try {
91
+
// Chromium already restricts access to certain globals in an
92
+
// iframe, this try catch block is to avoid
93
+
// "Failed to enumerate the properties of 'Storage': access is denied for this document"
94
+
keys.push(..._getOwnPropertyNames(target));
95
+
} catch (_error) {
96
+
keys.push(..._keys(target));
97
+
}
98
+
99
+
const seen = new Set();
84
100
for (let i = 0; i < keys.length; i++) {
85
101
const key = keys[i];
86
-
Object.defineProperty(standin, key, {
87
-
enumerable: true,
88
-
get: safeKey(target, key)
89
-
? () => {
90
-
return typeof target[key] === 'function' ||
91
-
typeof target[key] === 'object'
92
-
? withProxy(target[key])
93
-
: target[key];
94
-
}
95
-
: noop,
96
-
});
102
+
if (seen.has(key)) {
103
+
continue;
104
+
} else if (
105
+
key !== 'prototype' &&
106
+
(typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller'))
107
+
) {
108
+
seen.add(key);
109
+
const descriptor = _getOwnPropertyDescriptor(standin, key) || {};
110
+
if (descriptor.configurable) {
111
+
_defineProperty(standin, key, {
112
+
enumerable: descriptor.enumerable,
113
+
configurable: descriptor.configurable,
114
+
get: (() => {
115
+
if (!safeKey(target, key)) {
116
+
return noop;
117
+
} if (toplevel) {
118
+
try {
119
+
const value = mask(target[key], false);
120
+
return () => value;
121
+
} catch (_error) {
122
+
return noop;
123
+
}
124
+
} else {
125
+
return () => mask(target[key], false);
126
+
}
127
+
})(),
128
+
});
129
+
}
130
+
}
97
131
}
98
132
99
-
return standin;
133
+
if (standin.prototype != null) {
134
+
standin.prototype = _create(null);
135
+
}
136
+
137
+
return toplevel ? standin : freeze(standin);
100
138
}
101
139
102
140
let safeGlobal: Record<string | symbol, unknown> | void;
141
+
let vmGlobals: Record<string | symbol, unknown> = {};
103
142
104
143
function makeSafeGlobal() {
105
144
if (safeGlobal) {
···
114
153
115
154
// Get all available global names on `globalThis` and remove keys that are
116
155
// explicitly ignored
117
-
const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter(
156
+
const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter(
118
157
key => !ignore[key]
119
158
);
120
159
121
160
// When we're in the browser, we can go a step further and try to create a
122
161
// new JS context and globals in a separate iframe
123
-
let vmGlobals = trueGlobal;
162
+
vmGlobals = trueGlobal;
124
163
let iframe: HTMLIFrameElement | void;
125
164
if (typeof document !== 'undefined') {
126
165
try {
···
133
172
document.head.appendChild(iframe);
134
173
// We copy over all known globals (as seen on the original `globalThis`)
135
174
// from the new global we receive from the iframe
136
-
vmGlobals = Object.create(null);
175
+
vmGlobals = _create(null);
137
176
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
138
177
const key = trueGlobalKeys[i];
139
178
vmGlobals[key] = iframe.contentWindow![key];
···
141
180
} catch (_error) {
142
181
// When we're unsuccessful we revert to the original `globalThis`
143
182
vmGlobals = trueGlobal;
183
+
} finally {
144
184
if (iframe) iframe.remove();
145
185
}
186
+
} else if (typeof require === 'function') {
187
+
vmGlobals = _create(null);
188
+
const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports;
189
+
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
190
+
const key = trueGlobalKeys[i];
191
+
vmGlobals[key] = scriptGlobal[key];
192
+
}
146
193
}
147
194
148
-
safeGlobal = Object.create(null);
195
+
safeGlobal = _create(null);
149
196
150
197
// The safe global is initialised by copying all values from either `globalThis`
151
198
// or the isolated global. They're wrapped using `withProxy` which further disallows
152
199
// certain key accesses
153
200
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
154
201
const key = trueGlobalKeys[i];
155
-
safeGlobal[key] = withProxy(vmGlobals[key]);
202
+
safeGlobal[key] = mask(vmGlobals[key], true);
156
203
}
157
204
158
205
// We then reset all globals that are present on `globalThis` directly
159
206
for (const key in trueGlobal) safeGlobal[key] = undefined;
160
207
// We also reset all ignored keys explicitly
161
208
for (const key in ignore) safeGlobal[key] = undefined;
162
-
// Lastly, we also disallow certain property accesses on the safe global
163
-
safeGlobal = withProxy(safeGlobal!);
209
+
// It _might_ be safe to expose the Function constructor like this... who knows
210
+
safeGlobal!.Function = SafeFunction;
164
211
165
-
// We're now free to remove the iframe element, if we've used it
166
-
if (iframe) {
167
-
iframe.remove();
212
+
// Lastly, we also disallow certain property accesses on the safe global
213
+
// Wrap any given target with a Proxy preventing access to unscopables
214
+
if (typeof Proxy === 'function') {
215
+
// Wrap the target in a Proxy that disallows access to some keys
216
+
return (safeGlobal = new Proxy(safeGlobal!, {
217
+
// Return a value, if it's allowed to be returned and mask this value
218
+
get(target, _key) {
219
+
const key = safeKey(target, _key);
220
+
return !ignore[_key] && key !== undefined ? target[key] : undefined;
221
+
},
222
+
has(_target, _key) {
223
+
return true;
224
+
},
225
+
set: noop,
226
+
deleteProperty: noop,
227
+
defineProperty: noop,
228
+
getOwnPropertyDescriptor: noop,
229
+
}));
230
+
} else {
231
+
// NOTE: Some property accesses may leak through here without the Proxy
232
+
return freeze(safeGlobal!);
168
233
}
169
-
170
-
return safeGlobal;
171
234
}
172
235
173
236
interface SafeFunction {
···
179
242
const safeGlobal = makeSafeGlobal();
180
243
const code = args.pop();
181
244
245
+
// Retrieve Function constructor from vm globals
246
+
const Function = vmGlobals.Function as FunctionConstructor | void;
247
+
const Object = vmGlobals.Object as ObjectConstructor;
248
+
const createFunction = (Function || Object.constructor.constructor) as FunctionConstructor;
249
+
182
250
// We pass in our safe global and use it using `with` (ikr...)
183
251
// We then add a wrapper function for strict-mode and a few closing
184
252
// statements to prevent the code from escaping the `with` block;
185
-
const fn = new Function(
253
+
const fn = createFunction(
186
254
'globalThis',
187
255
...args,
188
256
'with (globalThis) {\n"use strict";\nreturn (function () {\n' +
+1
-1
tsconfig.json
+1
-1
tsconfig.json
+5
yarn.lock
+5
yarn.lock
···
136
136
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.4.tgz#48aedbf35efb3af1248e4cd4d792c730290cd5d6"
137
137
integrity sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==
138
138
139
+
"@types/node@^18.0.6":
140
+
version "18.0.6"
141
+
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7"
142
+
integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==
143
+
139
144
"@types/parse-json@^4.0.0":
140
145
version "4.0.0"
141
146
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"