“eval is evil” has become a maxim repeated in the Javascript community.
Douglas Crockford, in Javascript: The Good Parts, rightly advises against
hidden and explicit uses of eval for security and clarity reasons. Now, I find
eval
useful to implement DSLs in Javascript. The in-browser CoffeeScript
compiler wouldn’t be possible without eval
(directly or indirectly). So, in
this post, I wish to explore what appears interesting about eval
that is
relevant to building such DSLs.
For this post, I’ll stick to the behaviour of eval
in the Chrome browser
(i.e. the V8 engine, which also applies to Node.js). We’ll go through a
number of contexts and examine how eval
behaves in each of those. You can
copy paste the code shown here to Chrome’s JS console and run them.
What is eval
?
A simplistic description is that you pass a Javascript string to eval
and it
will “evaluate” it as Javascript code, whatever that means. The ECMA-262
specification (edition 5.1) has the following to say on eval
-
10.4.2 Entering Eval Code
The following steps are performed when control enters the execution context for eval code:
If there is no calling context or if the eval code is not being evaluated by a direct call (15.1.2.1.1) to the eval function then,
a. Initialise the execution context as if it was a global execution context using the eval code as C as described in 10.4.1.1.
Else,
a. Set the
ThisBinding
to the same value as theThisBinding
of the calling execution context.b. Set the LexicalEnvironment to the same value as the
LexicalEnvironment
of the calling execution context.c. Set the
VariableEnvironment
to the same value as theVariableEnvironment
of the calling execution context.If the eval code is strict code, then
a. Let
strictVarEnv
be the result of callingNewDeclarativeEnvironment
passing theLexicalEnvironment
as the argument.b. Set the
LexicalEnvironment
tostrictVarEnv
.c. Set the
VariableEnvironment
tostrictVarEnv
.Perform Declaration Binding Instantiation as described in 10.5 using the eval code.
10.4.2.1 Strict Mode Restrictions
The eval code cannot instantiate variable or function bindings in the variable environment of the calling context that invoked the eval if either the code of the calling context or the eval code is strict code. Instead such bindings are instantiated in a new VariableEnvironment that is only accessible to the eval code.
Introducing local variables
An expression of the form eval("var x = 10;")
is capable of introducing a new
variable x
in the context in which it is executed. However, as noted in the
ECMA specification, if the eval code is strict, then you cannot introduce a new
variable this way - i.e. eval("var x = 10;")
will work, but
eval('"use strict"; var x = 10;')
will not work. No exception is thrown, but the
variable is simply not introduced into the enclosing environment, though it is
available to the rest of the evaled code.
Consider the following function -
1 2 3 4 |
|
All of the following behave as one might expect -
localVars(10, "var y = 5;")
returns15
localVars(10, "var y = x + 5;")
returns25
.localVars(10, "'use strict'; var y = 5;")
raises aReferenceError: y is not defined
exception.
Capturing local variables in closures
Consider the following function -
1 2 3 4 |
|
captureSecretValue("secret")
returns 3.14159
as expected. You can also
create closures that capture the “secret” value -
1 2 |
|
However, the following gives a ReferenceError
-
1 2 3 4 5 6 7 8 9 10 11 |
|
This illustrates that only the variables in the lexical context are available
to eval
and not those in its evaluation context. The following will
therefore print 6.28318
as expected.
1 2 3 4 5 6 7 8 9 10 |
|
Scope objects
If you have an object whose keys give variable names and whose values give their values,
you can use eval
in conjunction with the with
statement (beware: evil squared!) to evaluate
code in that scope. Here is what I mean -
1 2 3 4 5 6 7 8 9 |
|
What is more interesting is that you can capture the “variables” in a closure that you create using eval as follows -
1 2 3 |
|
Since it is not the values of the variables that are being captured, but the references, you can now do -
1 2 |
|
If you subsequently delete one of the variables in the scope
object, you get
a ReferenceError
as one might expect. The scope
object therefore provides
access to the scope chain of the created closure. This interception is deep,
since you can introduce new variables into the scope by manipulating scope
as well.
1 2 3 4 5 6 |
|
Named functions within with
The following code doesn’t work and throws a ReferenceError
because
the inner
closure is instantiated outside the with
scope by the V8
compiler, contrary to what it might look like.
1 2 3 4 5 6 7 8 9 |
|
The following alternative works in V8 because the closure is created
when executing a statement within the with
clause.
1 2 3 4 5 6 7 8 9 |
|
This difference can be a WTF and points to the general recommendation of only
using the latter “name a function through assignment” approach. We know that
the definitions of named functions are lifted to the top of the surrounding
scope, but also know that they are lifted out of any surrounding with
blocks
as well.
Update: This inconsistency is a bug in V8 looks like. Firefox’s VM behaves consistently in both the cases above. I’ve submitted a V8 bug report for this problem.
Preventing access to global objects
In a browser environment, all global symbols are available as properties of the
window
object. We can use this, in conjunction with the “scope object”
feature as discussed above, to evaluate code that is to be prevented from
touching any of these global objects or classes. This gives us a poor man’s
sandbox.
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 |
|
The idea behind the above code is to prevent access to global properties
other than the ones given in the allowed
array. Furthermore, we also
don’t want the eval code to add new global properties by simple assignment,
for which we simply use strict mode evaluation.
Though this prevents access to existing global properties, it doesn’t prevent
access to properties that will be added to window
after the eval
happens.
To update the internal scope
object of the poorMansSandbox
, call it
once with no arguments before calling it on the string to be evaluated.
Of course, the eval-ed code can still do malicious things, but it cannot at least do them inadvertently. Hence “poor man’s”.
Conclusion
eval
should be used with tons of caution. However, if you’re interested in
making DSLs around Javascript, it helps to know its workings a bit deeper.
Remember - there is always something “good” in every “evil” ;)