-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathevaluators.js
249 lines (215 loc) · 8.96 KB
/
evaluators.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// Portions adapted from V8 - Copyright 2016 the V8 project authors.
// https://github.com/v8/v8/blob/master/src/builtins/builtins-function.cc
import { assert, throwTantrum } from './utilities';
import {
apply,
arrayConcat,
arrayJoin,
arrayPop,
getPrototypeOf,
regexpTest,
stringIncludes
} from './commons';
import { getOptimizableGlobals } from './optimizer';
import { buildScopeHandlerString } from './scopeHandler';
import { buildSafeEvalString } from './safeEval';
import { buildSafeFunctionString } from './safeFunction';
import { applyTransformsString } from './transforms';
import { rejectDangerousSourcesTransform } from './sourceParser';
function buildOptimizer(constants) {
// No need to build an oprimizer when there are no constants.
if (constants.length === 0) return '';
// Use 'this' to avoid going through the scope proxy, which is unecessary
// since the optimizer only needs references to the safe global.
return `const {${arrayJoin(constants, ',')}} = this;`;
}
function createScopedEvaluatorFactory(unsafeRec, constants) {
const { unsafeFunction } = unsafeRec;
const optimizer = buildOptimizer(constants);
// Create a function in sloppy mode, so that we can use 'with'. It returns
// a function in strict mode that evaluates the provided code using direct
// eval, and thus in strict mode in the same scope. We must be very careful
// to not create new names in this scope
// 1: we use 'with' (around a Proxy) to catch all free variable names. The
// first 'arguments[0]' holds the Proxy which safely wraps the safeGlobal
// 2: 'optimizer' catches common variable names for speed
// 3: The inner strict function is effectively passed two parameters:
// a) its arguments[0] is the source to be directly evaluated.
// b) its 'this' is the this binding seen by the code being
// directly evaluated.
// everything in the 'optimizer' string is looked up in the proxy
// (including an 'arguments[0]', which points at the Proxy). 'function' is
// a keyword, not a variable, so it is not looked up. then 'eval' is looked
// up in the proxy, that's the first time it is looked up after
// useUnsafeEvaluator is turned on, so the proxy returns the real the
// unsafeEval, which satisfies the IsDirectEvalTrap predicate, so it uses
// the direct eval and gets the lexical scope. The second 'arguments[0]' is
// looked up in the context of the inner function. The *contents* of
// arguments[0], because we're using direct eval, are looked up in the
// Proxy, by which point the useUnsafeEvaluator switch has been flipped
// back to 'false', so any instances of 'eval' in that string will get the
// safe evaluator.
return unsafeFunction(`
with (arguments[0]) {
${optimizer}
return function() {
'use strict';
return eval(arguments[0]);
};
}
`);
}
export function createSafeEvaluatorFactory(
unsafeRec,
safeGlobal,
transforms,
sloppyGlobals
) {
const { unsafeEval } = unsafeRec;
const applyTransforms = unsafeEval(applyTransformsString);
function factory(endowments = {}, options = {}) {
// todo clone all arguments passed to returned function
const localTransforms = options.transforms || [];
const realmTransforms = transforms || [];
const mandatoryTransforms = [rejectDangerousSourcesTransform];
const allTransforms = arrayConcat(
localTransforms,
realmTransforms,
mandatoryTransforms
);
function safeEvalOperation(src) {
let rewriterState = { src, endowments };
rewriterState = applyTransforms(rewriterState, allTransforms);
// Combine all optimizable globals.
const globalConstants = getOptimizableGlobals(
safeGlobal,
rewriterState.endowments
);
const localConstants = getOptimizableGlobals(rewriterState.endowments);
const constants = arrayConcat(globalConstants, localConstants);
const scopedEvaluatorFactory = createScopedEvaluatorFactory(
unsafeRec,
constants
);
const scopeHandler = unsafeEval(buildScopeHandlerString)(
unsafeRec,
safeGlobal,
rewriterState.endowments,
sloppyGlobals
);
const scopeProxyRevocable = Proxy.revocable({}, scopeHandler);
const scopeProxy = scopeProxyRevocable.proxy;
const scopedEvaluator = apply(scopedEvaluatorFactory, safeGlobal, [
scopeProxy
]);
scopeHandler.useUnsafeEvaluator = true;
let err;
try {
// Ensure that "this" resolves to the safe global.
return apply(scopedEvaluator, safeGlobal, [rewriterState.src]);
} catch (e) {
// stash the child-code error in hopes of debugging the internal failure
err = e;
throw e;
} finally {
if (scopeHandler.useUnsafeEvaluator) {
// the proxy switches this off immediately after ths
// first access, but if that's not the case we prevent
// further variable resolution on the scope and abort.
scopeProxyRevocable.revoke();
throwTantrum('handler did not revoke useUnsafeEvaluator', err);
}
}
}
return safeEvalOperation;
}
return factory;
}
export function createSafeEvaluator(unsafeRec, safeEvalOperation) {
const { unsafeEval, unsafeFunction } = unsafeRec;
const safeEval = unsafeEval(buildSafeEvalString)(
unsafeRec,
safeEvalOperation
);
assert(getPrototypeOf(safeEval).constructor !== Function, 'hide Function');
assert(
getPrototypeOf(safeEval).constructor !== unsafeFunction,
'hide unsafeFunction'
);
return safeEval;
}
export function createSafeEvaluatorWhichTakesEndowments(safeEvaluatorFactory) {
return (x, endowments, options = {}) =>
safeEvaluatorFactory(endowments, options)(x);
}
/**
* A safe version of the native Function which relies on
* the safety of evalEvaluator for confinement.
*/
export function createFunctionEvaluator(unsafeRec, safeEvalOperation) {
const { unsafeGlobal, unsafeEval, unsafeFunction } = unsafeRec;
function safeFunctionOperation(...params) {
const functionBody = `${arrayPop(params) || ''}`;
let functionParams = `${arrayJoin(params, ',')}`;
if (!regexpTest(/^[\w\s,]*$/, functionParams)) {
throw new SyntaxError(
'shim limitation: Function arg must be simple ASCII identifiers, possibly separated by commas: no default values, pattern matches, or non-ASCII parameter names'
);
// this protects against Matt Austin's clever attack:
// Function("arg=`", "/*body`){});({x: this/**/")
// which would turn into
// (function(arg=`
// /*``*/){
// /*body`){});({x: this/**/
// })
// which parses as a default argument of `\n/*``*/){\n/*body` , which
// is a pair of template literals back-to-back (so the first one
// nominally evaluates to the parser to use on the second one), which
// can't actually execute (because the first literal evals to a string,
// which can't be a parser function), but that doesn't matter because
// the function is bypassed entirely. When that gets evaluated, it
// defines (but does not invoke) a function, then evaluates a simple
// {x: this} expression, giving access to the safe global.
}
// Is this a real functionBody, or is someone attempting an injection
// attack? This will throw a SyntaxError if the string is not actually a
// function body. We coerce the body into a real string above to prevent
// someone from passing an object with a toString() that returns a safe
// string the first time, but an evil string the second time.
// eslint-disable-next-line no-new, new-cap
new unsafeFunction(functionBody);
if (stringIncludes(functionParams, ')')) {
// If the formal parameters string include ) - an illegal
// character - it may make the combined function expression
// compile. We avoid this problem by checking for this early on.
// note: v8 throws just like this does, but chrome accepts
// e.g. 'a = new Date()'
throw new unsafeGlobal.SyntaxError(
'shim limitation: Function arg string contains parenthesis'
);
// todo: shim integrity threat if they change SyntaxError
}
// todo: check to make sure this .length is safe. markm says safe.
if (functionParams.length > 0) {
// If the formal parameters include an unbalanced block comment, the
// function must be rejected. Since JavaScript does not allow nested
// comments we can include a trailing block comment to catch this.
functionParams += '\n/*``*/';
}
const src = `(function(${functionParams}){\n${functionBody}\n})`;
return safeEvalOperation(src);
}
const safeFunction = unsafeEval(buildSafeFunctionString)(
unsafeRec,
safeFunctionOperation
);
assert(
getPrototypeOf(safeFunction).constructor !== Function,
'hide Function'
);
assert(
getPrototypeOf(safeFunction).constructor !== unsafeFunction,
'hide unsafeFunction'
);
return safeFunction;
}