There was a question long ago much like this. So I dusted off some old code and fixed it up.
It essentially works by taking advantage of the with
keyword and providing it with a frozen empty object. The prototype of the empty object is filled with null
properties, the keys of which match the names global variables like self
, window
and their enumerable property keys; The prototype object is also frozen. eval
is then called within the with
statement (Almost the same way that scripts run with an implicit with(window){}
block if I understand correctly). When you try to access window
or its properties you get redirected (via the with
block) to null versions (with same key) found in empty object (or rather the empty object's prototype):
function buildQuarantinedEval(){
var empty=(function(){
var exceptionKeys = [
"eval", "Object", //need exceptions for these else error. (ie, 'Exception: redefining eval is deprecated')
"Number", "String", "Boolean", "RegExp", "JSON", "Date", "Array", "Math",
"this",
"strEval"
];
var forbiddenKeys=["window","self"];
var forbidden=Object.create(null);
[window,this,self].forEach(function(obj){
Object.getOwnPropertyNames(obj).forEach(function(key){
forbidden[key]=null;
});
//just making sure we get everything
Object.keys(obj).forEach(function(key){
forbidden[key]=null;
});
for(var key in obj){
forbidden[key]=null;
}
});
forbiddenKeys.forEach(function(key){
forbidden[key]=null;
});
exceptionKeys.forEach(function(key){
delete forbidden[key];
});
Object.freeze(forbidden);
var empty=Object.create(forbidden);
Object.freeze(empty);
return empty;
})();
return function(strEval){
return (function(empty,strEval){
try{
with(empty){
return eval(strEval);
}
}
catch(err){
return err.message;
}
}).call(empty,empty,strEval);
};
}
Setup by building a function/closure that evaluates some expression:
var qeval=buildQuarantinedEval();
qeval("'some expression'"); //evaluate
Tests:
var testBattery=[
"'abc'","8*8","console","window","location","XMLHttpRequest",
"console","eval('1+1+1')","eval('7/9+1')","Date.now()","document",
"/^http:/","JSON.stringify({a:0,b:1,c:2})","HTMLElement","typeof(window)",
"Object.keys(window)","Object.getOwnPropertyNames(window)",
"var result; try{result=window.location.href;}catch(err){result=err.message;}; result;",
"parseInt('z')","Math.random()",
"[1,2,3,4,8].reduce(function(p,c){return p+c;},0);"
];
var qeval=buildQuarantinedEval();
testBattery.map(function(code){
const pad=" ";
var result= qeval(code);
if(typeof(result)=="undefined")result= "undefined";
if(result===null)result= "null";
return (code+pad).slice(0,16)+": \t"+result;
}).join("\n");
Results:
/*
'abc' : abc
8*8 : 64
console : null
window : null
location : null
XMLHttpRequest : null
console : null
eval('1+1+1') : 3
eval('7/9+1') : 1.7777777777777777
Date.now() : 1415335338588
document : null
/^http:/ : /^http:/
JSON.stringify({: {"a":0,"b":1,"c":2}
HTMLElement : null
typeof(window) : object
Object.keys(wind: window is not an object
Object.getOwnPro: can't convert null to object
var result; try{: window is null
parseInt('z') : parseInt is not a function
Math.random() : 0.8405481658901747
[1,2,3,4,8].redu: 18
*/
Notes: This technique can fail when some properties of window are defined late (after initializing/creating our quarantined eval function). In the past, I've noticed some property keys are not enumerated until after you access the property, after which Object.keys
or Object.getOwnPropertyNames
will finally be able grab their keys. On the other hand this technique can also be quite aggressive in blocking objects/functions you do not want blocked (an example would be like parseInt
); In these cases, you'll need to manually add global objects/functions that you do want into the exceptionKeys array.
*edit* Additional considerations: How well this all performs depends entirely on how well the mask matches that of the property keys of the window object. Any time you add an element to the document and give it a new ID, you just inserted a new property into the global window object, potentially allowing our 'attacker' to grab it and break out of the quarantine/firewall we've setup (i.e. access element.querySelector then eventually window obj from there). So the mask (i.e., the variable forbidden) either needs to be updated constantly perhap with watch method or rebuilt each time; The former conflicts with the necessity of the mask to have a frozen interface, and the latter is kinda expensive having to enumerate all the keys of window for each evaluation.
Like I said earlier, this is mostly old code I was working on, then abandoned, that was quickly fixed up on short order. So it's not by any means thoroughly tested. I'll leave that to you.
and a jsfiddle
Ctrl
+Shift
+I
). – Gloxiniaeval
does not come from the user but "from anywhere else...". In parallel I do anything to prevent that this even happens! – Sigmundvar u = window; u["al"+"ert"]("hello");
– Sigmund