Matthew James
21/07/2024

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:

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:

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.

Screenshot of the Function Definitions section of the ECMA 262 specification

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.

Forrest Gump overlaid with the caption: I think I'll go home now

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... 👀