2017-10-01: Better alternatives than
eval
finally exist with await import()
syntax. You should definitely explore those. Additionally, LinkedIn has sunset Inject on their production site now that many viable alternatives exist. Hoo-ray modern web development!When it comes to executing code, eval()
is a terrible idea. However, somewhere in your coding career, you will need to evaluate code.
And unfortunately, if there's an error in the code, any line numbers are going to point to the invocation of eval()
and not the erroring line within the body itself. Lucky for us, <script>
tags correctly report erroring lines with line numbers.
Before we go any further, this is an obligatory don't use eval. It should be an option of last resort. Your Content Security Policy is hopefully already preventing you from doing this.
Trying to not Evaluate: .innerHTML
or .text
The first attempt was to set innerHTML
to your JavaScript, and then place that node onto the page via appendChild
. If you are only concerned with modern non-Microsoft browsers, you'd be done at this point.
// ⚠️ Obligatory THIS IS EVAL DO NOT DO THIS
function createScriptNode(code) {
var scr = document.createElement("script");
scr.type = "text/javascript";
scr.innerHTML = code;
return scr;
}
Internet Explorer won't execute the JavaScript inside of this script tag, even though the innerHTML
property is set. However, it uniquely supports the "text" property, which no other browsers seem to support. When set, scripts in IE will execute once appended to the DOM. A few changes to our above script, and we have a "safe" method. Borrowing from the idea of feature testing for things only IE supports, we'll actually feature test against this text property, falling back to alternate versions as needed.
// ⚠️ Obligatory THIS IS EVAL DO NOT DO THIS
function createScriptNode(code) {
var scr = document.createElement("script");
scr.type = "text/javascript";
try {
scr.text = code;
} catch (e) {
try {
scr.innerHTML = code;
} catch (ee) {
return false;
}
}
return scr;
}
Tying it Together
Further optimizations can be used to pre-select the best insertion method. In inject, we wrap the code we want to execute within a function declaration and assignment. This enables us to store the results of the eval
so that modules can then be executed on demand. The below example is just a simple JSON evaluator as a proof of concept. Like all things eval
, you should always be cautious with invoking it on items that are not 100% in your control.
// ⚠️ Obligatory THIS IS EVAL DO NOT DO THIS
var createScriptNode = (function () {
var testScr = document.createElement("script"),
property = "innerHTML";
testScr.type = "text/javascript";
try {
testScr.text = ";";
property = "text";
} catch (e) {}
return function (code) {
var scr = document.createElement("script");
scr.type = "text/javascript";
scr[property] = code;
return scr;
};
})();
var evalJSON = function (jsonString) {
var exec = ["window.results =", jsonString],
node = createScriptNode(exec),
results;
document.body.appendChild(node);
results = window.results;
delete window["results"];
return results;
};
While this code definitely makes it possible to do more harm than good (we're stepping around JSLint/Hint eval
checks), the upside is huge when you're evaluating code and need to understand at what line something is failing on. In the case of a module loading system, having both the upsides of a lazy eval
and the embedded script tag is a huge win.