我们希望让用户能够在我们的应用程序中执行自己创建的JavaScript代码。为此,我们需要使用eval
来评估代码。为了将所有安全问题降至最低(如果不是零),我们的想法是阻止在代码中使用任何window
或document
函数。所以没有XMLHttpRequest
或类似的东西。
这是代码:
function secure_eval(s) {
var ret;
(function(){
var copyXMLHttpRequest = XMLHttpRequest; // save orginal function in copy
XMLHttpRequest = undefined; // make orignal function unavailable
(function() {
var copyXMLHttpRequest; // prevent access to copy
try {
ret = eval(s)
} catch(e) {
console.log("syntax error or illegal function used");
}
}())
XMLHttpRequest = copyXMLHttpRequest; // restore original function
}())
return ret;
}
其工作原理如下:
secure_eval('new XMLHttpRequest()'); // ==> "illegal function used"
现在我有几个问题:
eval
的正确方法吗?window
和document
的哪些功能被认为是有害的?window
的所有(本机)函数但我无法枚举它们:例如,这不列出XMLHttpRequest
:
for( var x in window) {
if( window[x] instanceof Function) {
console.log(x);
}
}
有没有办法获取window
和document
的所有原生函数的列表?
修改
我的一个想法是在eval
内执行Worker
并阻止访问XMLHttpRequest
和document.createElement
(请参阅上面的解决方案)。这会(在我看来)产生以下后果:
document
window
你认为这里有任何缺点或泄漏吗?
EDIT2:
与此同时,我发现this question答案解决了我的许多问题以及我甚至没想过的一些事情(即浏览器死锁"while(true){}"
。
答案 0 :(得分:16)
您的代码实际上并未阻止使用XMLHttpRequest
。我可以使用以下方法实例化XMLHttpRequest
对象:
secure_eval("secure_eval = eval"); // Yep, this completely overwrites secure_eval.
secure_eval("XMLHttpRequest()");
或者:
secure_eval("new (window.open().XMLHttpRequest)()")
或者:
secure_eval("new (document.getElementById('frame').contentWindow.XMLHttpRequest)()")
第3种方法依赖于页面HTML中存在iframe
,有人可以通过在浏览器中操作DOM来添加。我不时地用Greasemonkey做这样的操作来消除烦恼或修复破坏的GUI。
我花了大约5分钟才弄明白,而且我绝不是一个安全大师。这些只是我能够迅速找到的漏洞,可能还有其他漏洞,我不知道。这里的教训是通过eval
确保代码真的非常难。
好的,所以使用Worker
来运行代码将会处理上面的第2和第3个案例,因为Worker
中没有可访问的窗口。并且......嗯..第一种情况可以通过在其范围内隐藏secure_eval
来处理。故事结局?如果只是......
如果我将secure_eval
放入网络工作者并运行以下代码,我可以重新获取XMLHttpRequest
:
secure_eval("var old_log = console.log; console.log = function () { foo = XMLHttpRequest; old_log.apply(this, arguments); };");
console.log("blah");
console.log(secure_eval("foo"));
原则是覆盖在secure_eval
之外使用的函数来捕获XMLHttpRequest
,方法是将其分配给一个故意泄漏到全局空间的变量。 worker,等到secure_eval
之外的工作程序使用该函数,然后获取保存的值。上面的第一个console.log
模拟了secure_eval
之外的篡改功能的使用,第二个console.log
显示了捕获的值。我使用过console.log
,为什么不呢?但是真的全局空间中的任何函数都可以这样修改。
实际上,为什么要等到工人可以使用我们篡改的某些功能?这是另一种更好,更快捷的访问方式XMLHttpRequest
:
secure_eval("setTimeout(function () { console.log(XMLHttpRequest);}, 0);");
即使在工作人员(具有原始console.log
)中,这也会将XMLHttpRequest
的实际值输出到控制台。我还要注意,传递给this
的函数内setTimeout
的值是全局范围对象(即不在工作者中的window
,或self
在工人中),不受任何变量阴影的影响。
解决方案here怎么样?好多了,但在Chrome 38中运行时还有一个漏洞:
makeWorkerExecuteSomeCode('event.target.XMLHttpRequest',
function (answer) { console.log( answer ); });
这将显示:
function XMLHttpRequest() { [native code] }
同样,我没有任何安全大师或破解者一心想要制造麻烦。可能还有更多我没想过的方法。
答案 1 :(得分:2)
我会在这里按顺序回答你的问题。
此模式是否是保护
eval
的正确方法?
这部分略显主观。我没有看到任何重大的安全缺陷。我尝试了几种访问XMLHttpRequest
的方法,但我不能:
secure_eval('XMLHttpRequest')
secure_eval('window.XMLHttpRequest')
secure_eval('eval("XMLHttpRequest")()')
secure_eval('window.__proto__.XMLHttpRequest') // nope, it's not inherited
但是,如果你想将更多内容列入黑名单,那将会很多。
window
和document
的哪些功能被认为是有害的?
这取决于你认为“有害”的东西。如果DOM完全可访问,那会不好?或者WebKit桌面通知或语音合成呢?
您必须根据具体用例做出决定。
发送问题2.我试图掩盖
window
的所有(本机)函数,但我无法枚举它们:
这是因为大多数方法都是不可枚举的。要枚举,您可以使用Object.getOwnPropertyNames(window)
:
var globals = Object.getOwnPropertyNames(window);
for (var i = 0; i < globals.length; i++) {
if( window[globals[i]] instanceof Function) {
console.log(globals[i]);
}
}
我的一个想法是在
eval
内执行Worker
并阻止访问XMLHttpRequest
和document.createElement
(请参阅上面的解决方案)。
这听起来不错。
答案 2 :(得分:2)
我偶然发现了一篇关于臭名昭着的Eval
here的非常好的博客文章。文章确实详细讨论过。您无法减轻所有安全问题,但您可以通过为输入构建令牌来阻止跨脚本攻击。从理论上讲,这可以防止可能有害的恶意代码被引入。
你唯一的另一个障碍就是中间人攻击。我不确定这是否可能,因为你不能信任输入和输出。
Mozilla Developer Network明确说明:
eval()是一个危险的函数,它执行它传递的代码 具有来电者的特权。如果使用字符串运行eval() 可能会受到恶意方的影响,您最终可能会运行 具有您的权限的用户计算机上的恶意代码 网页/扩展程序。更重要的是,第三方代码可以看到 调用eval()的范围,可能导致可能的攻击 以类似函数不易受影响的方式。
eval()通常也比替代品慢,因为它必须 调用JS解释器,同时优化许多其他构造 通过现代JS引擎。
对于常见的eval()有更安全(和更快!)的替代方案 用例。
我稍微反对Eval
,并在保证时真正尝试使用它。
答案 3 :(得分:2)
很久以前有一个问题很像这样。所以我把一些旧代码掸掉并修好了。
它本质上是通过利用with
关键字并为其提供冻结的空对象来实现的。空对象的原型填充了null
属性,其键与名称全局变量匹配,如self
,window
及其可枚举属性键;原型对象也被冻结。然后在eval
语句中调用with
(如果我理解正确的话,几乎与脚本使用隐式with(window){}
块运行的方式相同)。当您尝试访问window
或其属性时,您将被重定向(通过with
块)到空对象中找到的null版本(使用相同的键)(或者更确切地说是空对象的原型) :
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);
};
}
通过构建一个评估某个表达式的函数/闭包来进行设置:
var qeval=buildQuarantinedEval();
qeval("'some expression'"); //evaluate
试验:
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");
结果:
/*
'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
*/
注意:当窗口的某些属性定义较晚时(在初始化/创建隔离的eval函数之后),此技术可能会失败。在过去,我注意到在您访问该媒体资源之前不会枚举某些属性键,之后Object.keys
或Object.getOwnPropertyNames
最终会获取其密钥。另一方面,这种技术在阻止你不想阻止的对象/函数方面也非常积极(例如parseInt
);在这些情况下,您需要手动将您想要的全局对象/函数添加到 exceptionKeys 数组中。
* edit * 其他注意事项:这一切的执行情况完全取决于蒙版与窗口对象的属性键的匹配程度。每当您向文档添加元素并为其提供新ID时,您只需将新属性插入到全局窗口对象中,这可能会让我们的攻击者&#39;抓住它并打破我们设置的隔离/防火墙(即访问element.querySelector然后最终从那里窗口obj)。所以掩码(即变量禁止)要么需要不断更新,也不需要每次都重建;前者与掩码必须具有冻结界面冲突,后者有点昂贵,必须枚举每个评估窗口的所有键。
就像我之前说过的那样,这主要是我正在处理的旧代码,然后被放弃了,很快就修好了。所以它绝不是经过彻底测试的。我会把它留给你。
答案 4 :(得分:2)
我已在我的问题中说明了这一点,但为了更清楚,我会将其作为答案发布:
我认为this question上接受的答案是完全隔离和约束eval()
的唯一方法。
对这些黑客也是安全的:
(new ('hello'.constructor.constructor)('alert("hello from global");'))()
(function(){return this;})().alert("hello again from global!");
while(true){} // if no worker --> R.I.P. browser tab
Array(5000000000).join("adasdadadasd") // memory --> boom!