You and I are about to go on a journey of rediscovery. We're going to move fast and dirty through the trenches of HTML, ECMA, and their love-child: JavaScript. The journey is not a straight path and will be fraught with danger: there is a lot of ground to cover, some mountains of information that we'll need to navigate, as well as a few classic here be dragons that we need to avoid.
This journey will no doubt take us to some dark places, and I am unsure whether we will end atop the tallest peak or within the darkest dungeon. While the end answer is far from sight all stories must start at the beginning, and this one is no different. So strap your helmet on and get your favourite glue and spoon out because we're going to explore why the onclick
HTML attribute (and friends) are basically eval
.
And that's a bad thing?
Eval comes with a fair bit of emotional baggage, most of which is unsightly or ill-favoured. Even the cases arguing for eval
generally comment saying it is just misunderstood, like a teenager going through a phase. The main points of debated contention are:
- Security - XSS is technically possible in the browser but the bigger concern is running it on a Node.js server.
eval
elevates privileges and has specific "unsafe-eval" CSP restrictions. - Scoping -
eval
has access to all variables/functions in the current scope and ALL parents scopes. This means it can access or change them entirely (this includes reading cookies). No surprise that replacingeval
with alternatives can alleviate memory utilisation. - Debugability - Historically impossible, then it became possible but was still a nightmare to debug, now in more modern times debugging
eval
code is less of an issue but it is still not great. - Performance -
eval
invokes the JavaScript interpreter which will always incur a performance hit, while many other JavaScript constructs are optimized by modern JavaScript engines. However in some cases usingeval
can be much faster than the alternatives. - Minifying - Good luck! Most minifiers won't touch any code that contains significant usage/dependence on
eval
. Variable naming is changed in minification, but the variables in the eval string are not. Thus minifiers have two options: everything breaks or nothing is minified - neither are great choices. - I don't feel like hiking this knowledge mountain any longer so I'm turning around and leaving it at that.
The general sentiment on eval
is not good, even if some of the allegations against it are actually non-issues. Further discussions from the arcane wizards of the day are not hidden, and many more readings can be found on the science of eval
should you go looking. The most prominent incantation used is this:
eval is evil
Don't know how many times I've read that, but it has got to be in the hundreds. The mantra is most often attributed to Douglas Crockford who still has the statement at the end of his Code Conventions blog post. Being compared to eval
is like being compared to your local tweaker that sniffs around the dumpster at the servo - not exactly something that you should aspire for. But Douglas Crockford, among others, don't limit their scorn to just eval
...
Indirectly poking the bear
Invoking eval
indirectly produces different results than directly calling eval
. Uhh, okay then, what the hell does invoking it indirectly mean? Luckily we have the campfire of the wise sage MDN to show us some examples:
// Direct call
eval("x + y");
// Indirect call using the comma operator to return eval
(0, eval)("x + y");
// Indirect call through optional chaining
eval?.("x + y");
// Indirect call using a variable to store and return eval
const geval = eval;
geval("x + y");
// Indirect call through member access
const obj = { eval };
obj.eval("x + y");
This rare way of calling eval
makes it behave mostly the same as an external script. Most importantly though, indirect eval
incantations operate in the global scope not the local scope. This means the code being eval
'ed doesn't have access to any local variables or functions (and therefore cannot read or change them). Ponder this:
function test() {
const x = 2;
const y = 4;
// Direct call, uses local scope
console.log(eval("x + y")); // Result is 6
// Indirect call, uses global scope
console.log(eval?.("x + y")); // Throws because x is not defined in global scope
}
But don't ponder too much, we still have a ways to go in this journey. Divining the tea leaves that MDN has given us we can deduce that indirect eval
is just eval++
. Slightly more secure and slightly more characters typed. It is an upgrade that is rather easy to miss. The pit of success here is more like a divot that you sometimes roll your ankle on. Speaking of rolling on, we have dallied here too long, onwards, to enlightenment.
Friends in low places
The next curiosity we shall examine is the Function
constructor. It's a plump creature hidden in plain sight. It's face and frontal region displays a convincing mane of familiarity and productivity. Behind its back it has a talon ready to shank you for not reading the docs well enough. Indeed, further investigation will reveal that this creature is just an indirect eval
with lipstick on, dressed in a wig, with a talon which is actually just a prison shank. If that leaves you hot and heavy, keep your pants on because we are about to mud wrestle this piggy creature to see what it is really made of.
// Just a string
const hello = new Function('return "Hello, world"');
console.log(hello()); // Expected output: Hello, world
// Arguments plus string
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6)); // Expected output: 8
Well, that certainly looks similar to eval
. Furthermore, just like indirect eval
the Function
constructor only has access to the global scope. This gives it all the same advantages of indirect eval, plus an extra unique advantage:
- No access to local scope.
- Smaller memory footprint.
- Not treated as an External Script, but as a function body.
- Thus, can use return statement.
Because of these facts those with lesser conviction will tell you that it is the cure for all eval
usage. But don't be fooled by the lipstick, that hidden shank is ready and its aim is true. Lets poke this creature and find out how it reacts:
// Make some global variables
var fun, a;
(function () {
// override the global vars in the same scope
var a = 123;
fun = new Function("return a");
})();
fun(); // undefined
a = "global"
fun(); // "global"
Now we see its feisty little gotchas itching to jab us. Of course it is not just that functions created by the Function
constructor don't keep a reference to the environment they were defined in, there's a whole slew of extra code smell you get for free. Let's tantalize our senses:
The Opera team points out that the Function
constructor is slow:
Each time eval or the Function constructor is called on a string representing source code, the script engine must start the machinery that converts the source code to executable code. This is usually expensive for performance - easily a hundred times more expensive than a simple function call, for example.
...
If you want a function, use a function
Douglas Crockford, to no one's surprise, is also against using this:
new Function(strings...)
Do not use this form. The quoting conventions of the language make it very difficult to correctly express a function body as a string. In the string form, early error checking cannot be done. It is slow because the compiler must be invoked every time the constructor is called. And it is wasteful of memory because each function requires its own independent implementation.
MDN correctly points out that the Function
constructor is still classified as an Unsafe eval expression in terms of Content Security Policy
The'unsafe-eval'
source expression controls several script execution methods that create code from strings. If a page has a CSP header and'unsafe-eval'
isn't specified with the script-src directive, the following methods are blocked and won't have any effect:
...
Function()
And of course it is one of the first things to avoid when considering malicious injection attacks.
Preventive measures for JavaScript code injection attacks include:
Avoiding the use of unsafe characters and functions including the eval(), setInterval(), setTimeout() and function constructor statements at the user input field
Don't get distracted by the mention of setInterval()
and setTimeout()
, we've too much ground left to cover. Those mentioned functions are just as bad, but no time to explain! We mustn't stray from the path. The Function
constructor is not god's gift to the world and it is a far cry from the saviour that was promised. With that established, victory's cradle beckons.
What does this have to do with HTML attributes?
Shh, you'll wake the dragons. We're moving to the target but one wrong step and we'll be stuck in the ECMA specification for the rest of known time. While we must be careful we must also never stop. Our first calculated and deliberate foray into specifications is the living html standard by WHATWG. Specifically lets take a peep at the onclick handler.
Deciphering the occult scroll it is revealed to us that the onclick
attribute has a value of Event handler content attribute
. Knowledge calls and we must answer. Steady your feet as we venture further into this pit of specifications. Digesting the specification entry for Event handler content attribute
divulges to us a truth many are not ready to confront.
Event handler content attributes, when specified, must contain valid JavaScript code which, when parsed, would match the FunctionBody production after automatic semicolon insertion.
A deep veracity is contained within this scientific riddle. Let us break it down and gorge ourselves on its truthful insides:
Event handler content attributes, when specified, must contain valid JavaScript code...
Verbose but ultimately helpful. Take away the words between the commas and we have the meaning. The Event handler content attribute
must contain valid JavaScript code. Simple enough, lets continue:
which, when parsed, would match the FunctionBody production
Now this is some peculiar prose. Firstly, this states that the Event handler content attribute
must be parsed. Curious, that sounds like eval
to me. Secondly, the FunctionBody is a direct match of that parsed code. Said differently for the dull among us: the FunctionBody is set to the parsed, valid JavaScript code. That sounds like the Function
constructor. But what exactly is a FunctionBody
? And what of the rest of the quote?
after automatic semicolon insertion.
This must be some sort of mistake. Automatic Semicolon Insertion (ASI) sounds like a terrible idea that would lead to only misunderstandings and widespread suffering. Best to leave that sleeping dragon lie. Pretend ASI doesn't exist, until it bites you, it is what all other programmers before you have done. Back to the task at hand.
We have made a serious discovery here. The assertion is that an Event handler content attribute
is effectively a synonym for the Function
constructor, or possibly even for eval
. However, let us not be too hasty. In JavaScript things can be strange, for example: Not a Number
is actually a Number
. Thus, more excavation of the specs is needed. We must confirm or deny this assertion by finding out what the hell a FunctionBody
actually is.
Just a quick peep into the definition of FunctionBody
should do. Just to tickle your fancy...
Should Have Read the Manual
"Abandon all hope ye who enter." - Dante Alighieri, Inferno
Well shit, now we've done it. Like the dwarves of Middle Earth our greedy hubris has taken us into the ECMA specifications. There's nothing for it, we must take what we need and get out of here as calmly and purposely as possible.
As it turns out, these scriptures don't lie, and they show us that a FunctionBody
is, unfortunately, accurately named and described. FunctionBody
is the part of the function that runs when called. The code that is after the FormalParameters and contained within open and closing braces.
Things are looking bleak. Maybe there is something that has been overlooked? Revisiting the Event handler content attribute
may provide other information. Peering back into the specification void we can glean that if the value of the eventHandler
is not null then its value get set to an internal raw uncompiled handler. The final nail in the coffin. The assertion is true, onclick handlers are just Function
constructors, and all hope is dead.
Peer into the crypt for further dissection if you must, but for me, this journey into the darkest dungeon has come to the end.
Lessons Learnt
Like the journal of a traumatised child lets describe what we've witnessed:
eval
can be evil.
Indirect eval
is just eval++.
The Function
constructor is just indirect eval
.
onclick
is just an alias of the Function
constructor.
Thus, onclick
is basically just eval
.
With all of that said, it is lucky then that no one uses the onclick
, onchange
or any of the on*
HTML attributes anymore right? That sure would be rough if that were still the case... 👀