The problem with eval
eval is not necessary evil — you just need to make sure you’re dealing with
The problem lies in that “just”, as you can’t 100% guarantee that the source of the eval-ed code (a DB, a file) won’t get compromised at any point in time.
Long story short: consider all code that goes through an
Here’s some funny things
eval can do:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
The last example is the one I like the most: there an attacker would re-define
eval function itself, causing the application to crash (
TypeError: eval is not a function)
the next time that block of code is executed.
Now that we’ve seen some basic examples on how you could easily tear down an
application that uses
eval too eagerly, let’s take a step back and try
to figure out when it could be a good idea to execute “external” code
Free the code!
Ever heard of the expression language, or EL?
It’s kind of a specification that defines a programming language used to evaluate expressions such as:
as opposed to having to deal with the syntax and quirks of each and every language — think of PHP’s dollar sign and weird arrow-syntax:
Long story short, EL is a very lightweight programming language that provides convenient shortcuts so that any non-technical person can write code: initially thought of as a nicer replacement for writing regular code into HTML templates, it’s been widely used in the Java ecosystem and the Symfony2 framework popularized it in the PHP world.
Why am I telling you this? Well, because it’s damn convenient, as you can let non-programmers customize aspects of your applications through these expressions. For example, imagine someone from your SEO team giving you a human-readable YAML file like the following:
1 2 3 4 5 6
You can then import it in your application and add keywords to your webpage based on those conditions — with code that would look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
The advantage of using an expression language is that you don’t need to build a full fledged CMS to customize various parts of your applications — import a file (or a spreadsheet) and you’re done.
1 2 3 4 5 6 7
These are all valid examples of JS code: some operators
work a little bit differently (for example
in will mainly work with
objects, not arrays) but, if you’re not too creative, you can basically make
sure your expressions are valid JS that can be executed without a custom
At Namshi, for example, we allow our marketing team to create voucher codes based on expressions that look like:
1 2 3
(this would apply a 10% discount if the customer purchases at least 50 USD in Nike products)
Now, you might think you just found the holy grail that allows you to avoid
building a complicated CMS and let stakeholders write that code for you — so
you start hacking around, come up with a prototype, everyone is extremely stoked
and it goes into production within a few days. Now your SEO guy can customize
all of the metatags in your pages by simply requesting you to deploy an updated
Then, disaster strikes.
Eval is not a sandbox
Even assuming your SEO guy is responsible enough to not make basic mistakes such
as toying around with the file and forgetting a
while loop in one of the conditions,
you’re still facing a potentially high security threat — a user
might steal the email credentials of our SEO hero and send you a new version of
seo-rules.yml that contains dangerous code, asking you to deploy those changes
urgently. Since you trust the SEO guy, you don’t even review it, go live and…
The way EL has been implemented in Java, PHP and other languages is within the context of a sandbox, where “accidental” code cannot do harm outside of the expression. These languages have lexers, parsers and tokenizers that convert each and every bit of the expression into a tree and execute it “safely”, without letting it access variables, functions or APIs that haven’t been defined while building the expression itself.
Simply put, let’s look at the following example:
An expression evaluator first tries to make sure the
expressions is syntactically valid and then figures out if all its components
are defined: in this case
d need to be defined
within the context of the expression to be able to execute it.
That makes it so that, generally, we will be required to invoke the expression like this:
If the expression uses a variable that is not defined within its context, the evaluation
will then throw an error, which is exactly what prevents code from breaking out of its intended scope to begin
fs and so on won’t be available for an attacker to take
eval directly executes code and has access to the same scope it’s
been called from2: since it’s missing a sandboxing feature,
it is a very risky and, in my opinion, a poor choice for implementing an expression
Now, imagine you could run some JS code in a new “node process” that has no access
to the standard node library — no
console, just basic plain JS: that’s
vm module lets you do.
Let’s take a look at this example:
1 2 3 4 5
Here we are asking Node to create a new V8 context and run a bunch of code
a + 1) there for us, passing an object that constitutes the global environment of
that new context (
Note that the current execution context is not affected by what happens in that new context:
1 2 3 4
unless you specifically tell the VM that you want to re-use an existing context
or to use the current context with
1 2 3 4
When you use the current execution context (
the executed code is going to have access to globally-defined variables of the current
context which, of course, exposes us to the same kind of problems we’d have with
1 2 3 4
So, let’s stick to running our code on brand new execution contexts through
A nice “feature” of new contexts is that, by default, they can only be used to execute plain old JS, as they don’t have access to functions / node modules unless you inject those in the context.
You can try it out for yourself in node’s REPL:
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
One of the hidden features of the
vm module is that it uses implicit returns,
which means that the return value of the expression is available to the main
execution context. As we’ve already seen:
In fact, if you try to return “manually” you’ll get a slap in the face:
1 2 3 4 5 6
But implicit returns don’t just stop there — as you can write multiple
vm will make sure you get the return value of the last
block, even if you split your “code” into multiple lines:
1 2 3 4 5 6 7
The other killer feature of the
vm module is that it’s able to specify
timeouts for the scripts it executes:
1 2 3 4 5 6 7 8 9 10 11 12 13
In the above scenario, we’re giving the script 3 milliseconds to execute
and, since it’s trying to execute an infinite
an error and gives us an opportunity to catch it:
1 2 3 4 5 6 7 8
It’s worth noting that the VM API is synchronous, so please be mindful when assigning timeouts as you might end up stalling for too long.
Of course, running code in a separate context requires quite some
overhead compared to running an
eval: we need V8 to setup the context,
compile the code on-the-fly and, finally, execute it — and the whole process
doesn’t come very cheap.
Let’s look at a simple benchmark that loops 1k times over an eval / vm instruction and records the total time it takes for the loop to complete3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Even though this benchmark is not very scientific, it should give us a good idea of the differences between the two, if any:
1 2 3
Holy moly, we’re talking 1ms vs half a second here!
We could probably get some better results if we re-use contexts rather than creating new ones in each and every loop cycle but, at the end of the day, I think that would be pointless as this is, of course, a provoking benchmark — never trade security for speed.
In addition, I truly don’t think you’ll be evaluating thousands of expressions on every request, so the slowdown of using the VM module might look more like this:
1 2 3
Still expensive, but very minimal in the context of a request / response cycle4.
Are we good to go using VM? Surprise surprise!
You came all the way down here thinking
vm solved all of your problems, just
like I did: when it comes to security, though, I always want to double and triple
check to make sure I’m really considering the safest solution and, after some
digging around, I had one of those facepalm moments, as vm might not be safe
Consider this trick:
Unfortunately, this is a valid exploit that will likely never get fixed — the
VM module is to be considered a sandbox, not a jail,
meaning that it can’t really screw around with the current context but it can
very well access the standard JS APIs and the global NodeJS environment, providing a straightforward attack vector
similar to what you’d end up with by using
One way to make sure that VM can’t use this funny trick to access globals is by making sure the context is only made of primitives:
1 2 3
What we’re doing here is to create a “special” context that does not have a
Object.create(null)), thus removing the ability to access
constructors and prototypes:
1 2 3 4 5
The above code will throw the
ReferenceError: process is not defined error, but will
still be vulnerable if we add non-primitives in the context:
1 2 3
Unless you can afford to only use primitives in our context, we’re back to square one, left with no way to safely execute untrusted JS code.
And, by the way, this isn’t likely to change soon as it’s been there for ages.
Worse of all, most people still assume
vm is safe (see here
which means there might be tons of applications out there that are vulnerable to
this kind of attack.
As I briefly explained in the previous paragraph, I was doing some digging
around to see if
vm’s sandbox could still be exploited when I found myself
on this gist which
eventually led me to the VM2 library on github.
Curious on what would this module add on top of
vm’s default behavior,
only to find that it actually secures the sandbox through some custom security checks (mainly using proxies):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Sweet — we can simply then install VM2
and start switching all
vm.runInNewContext(...) to VM2’s API:
1 2 3
At this point you could probably settle on VM2 and call it a day, but you’d still need to ask yourself “what if VM2 contains a vulnerability?”.
All in all, there have been a few security concerns with this module as well, and similar libraries had the same problems — to be honest, my gut feeling is that a new attack vector might be out there, waiting to be discovered.
It’s been quite a long read if you’ve made it this far, so let me leave you with some key takeaways:
- there are business cases for evaluating external code, on-the-fly
- avoid using
evalfor that, it’s not safe at all
vmmodule provides a safer implementation, but it can still be exploited by an attacker
- VM2 appears to provide a more solid sandbox that can’t be escaped, but a security issue might lurk somewhere in the codebase…
All in all I think the only safe way to run untrusted code is to “physically” separate your application from that code by, for example, running it in a VM, a docker container or a lambda function on AWS5. If you can’t go for this kind of isolation, then I would recommend you to settle on VM2.
Last but not least: what about the browser?
Some believe eval isn’t such a threat
on the browser, as most clients can anyhow do the same kind of harm through the
DevTools’ console. Even though, in principle, that’s true, there are some other
things to consider
that might still make
eval a risky element of your codebase.
One very interesting approach is to use web workers, as they provide a semi-isolated context that cannot interfere with the original window.
That said, there’s still a long way to go until we can safely run an untrusted piece of code, both on the client and the server.
Perhaps that’s for the best ;–)
- Another example, you should use ‘&&’ and not ‘and’ ↩
- including the global one because you can just use global.$VAR in Node ¯\_(ツ)_/¯ ↩
- console.time|timeEnd are amazing for this kind of quick benchmarks ↩
- Unless you are, of course, optimizing for each and every ms. In general, I tend to forget about these optimizations as the bottleneck is usually somewhere in the network, or a DB query, so optimizing for that won’t really move the needle ↩
- Using lambda for this would actually make it for a cool proof of concept ↩