由于 Douglas 没有在他大部分的做品中标明时间,目前尚不清楚是不是他真正的独创了这个词,由于在 2003年发表的一篇文章中也使用了这个词但却并无说起他。无论怎样,如今人们在代码看到 eval 就像看到汇编时代的 go-to 语句同样,不知道他们是否真的理解了它的用法。
尽管流行的理论(同时也是 Crockfor 的坚持),仅仅存在 eval() 并不能说明问题。使用 eval()不会自动向你开放跨域脚本攻击(XSS),也不意味着一些你不了解的挥之不去的安全漏洞。像其余工具同样,你须要知道的是如何正确的使用它,但即便你使用的不正确,潜在的危险仍然至关低的,能够接受。
在 “eval 是魔鬼”创造出来的时候,这个短语就成为了人们滥用的源泉,这些人大部分是那些不了解 javascript 的语言特性。令人惊讶的是这些滥用不是来性能或是安全有关,而是在于不理解或是不会构造 javascript 中的引用。假设,你有几个表单输入,包含一个数字字段,如“option1” 和 “option2”,常见的用法是:
function isChecked(optionNumber) {
return eval("forms[0].option" + optionNumber + ".checked");
}
var result = isChecked(1);
在这种状况下,开发者试图用 forms[0].option1.checked, 但并不知道怎么样用 evla() 办到。你会看到那些有着10年或以上开发经历的程序员写的代码中有不少像上面这样的写法,这些时候他们每每还不理解怎么正确的使用这门语言。这里函数 eval 的用法是不正确的不是由于它不是必须的而是由于这种写法很很差。你能够很简单重写这个函数:
function isChecked(optionNumber) {
return forms[0]["option" + optionNumber].checked;
}
var result = isChecked(1);
在大多数涉及这种性质的状况下,你能够调用 eval 来代替上面的写法,只须要经过括号来构造正确的属性名称(也就是说,毕竟这也是它存在的缘由之一)。那些早期写博客谈论 eval 滥用的人,包括Crockford,讨论的大多就是这种模式。
可调试性
避免使用 eval 的一个好的理由是其缺少调试功能。直到最近,若是代码中出现问题仍是不能够进入 eval() 语句的单步调试状态。这就意味着你的代码进入到一个黑箱状态而后直接输出。 Chrome 的开发者工具如今已经能够调试 eval()事后的代码,但使用过程仍然是痛苦的。你必须等到执行过一次而且出如今源面板以前。避开 eval() 事后的代码能使调试更加容易,让你查看和跟踪代码时更加简单。但这不会使 eval() 变得邪恶,只是不可避免的一点是在正常的开发流程中会出现一点小问题。
性能
另外一个对 eval 的打击来自其对性能的影响。在较老的浏览器中,你的代码会被两次编译,也就是说你的代码被编译事后,eval()语句里的代码还要被编译一次。在一些没有内置 javascritp 编译引擎的浏览器中,程序的性能会慢10倍(甚至更糟)。
虽然如今出现了新一代的 javascript 编译引擎, eval()仍是存在一些问题。大多数引擎运行代码的方式有两种:快速路径或者慢速路径。快速路径的代码是稳定的而且是可预测的,所以被编译后执行速度更快。慢速路径代码不可预测的,所以很难进行编译而且在执行过程当中还须要再编译[3]。若是你的代码中出现了 eval() 那就意味着代码是不可预测的,所以也须要在编译器中运行——运行起来就像是在“老浏览器”中的速度,而不是“新浏览器”的速度(再一次,一个10倍的差别)。
另外,eval()的存在使 YUI 压缩工具不能在调用 eval() 函数域的时候替换其内的变量名称。由于 eval() 能够直接访问任何一个变量。重命名它们将致使错误(其余工具像 Closer Compiler 或者 UglifyJS 可能仍然会替换这些变量名——最终也会致使错误)。
因此在使用 eval() 的时候,性能仍然是一个大问题。再一次,这也很难让它变得邪恶,只是一个须要谨记的警告而已。
安全性
一旦讨论 eval() 的时候,一个必打的王牌就是安全性。一般这些谈话会从 XSS 攻击开始,而后讨论 eval() 是怎样将你的代码开放给了它们。表面上看,这些困惑是值得理解的。由于经过 eval() 的定义,它能够在一个页面上下文中执行任意的代码。这时若是你将用户的输入直接拿来执行是很危险的。可是若是你的输入不是来自用户的,还有什么真正的危险吗?
我已经收到了不止一我的来投诉我写的 CSS 解释器的一段代码,这段代码中就是使用了 eval()[4]。这段有争议的代码使用 eval()将一个字符串标记从 CSS 变成 javascript 的字符串的值。为了简化我设计的字符串解析器,这是最简单的方法去获得真正的字符串值。到目前为止,尚未人可以或者愿意生成一个攻击场景来让这段代码陷入麻烦,这是由于:
1. 执行过 eval() 事后的返回值来自于这个命令的发起者;
2. 命令的发起人已经验证过而且证实这是一段有效的字符串;
3. 代码的执行基本上都在命令行;
4. 即便是在浏览器端执行,这段代码也是被封装在一个闭包内,不能被直接调用。
固然,这段代码有一个直接访问命令行地址的原语,这也让问题有点复杂。
设计在浏览器端执行的代码须要面临许多不一样的问题,可是,eval() 的安全显然不在其中。再一次要说明的是,若是你正在使用用户的输入而且将其做为 eval() 的执行参数,那么你就是在自找麻烦。千万不要这么作。可是,若是你调用 eval() 时的输入是通过你本身控制的而且不让用户更改,那么这样就不会出现安全问题。
最近这些天最多见的被引用来攻击 eval() 的向量来自于从服务端返回的 eval()ing 中的代码。这个模式经过引入著名的 JSON 而开始流行起来,迅速流行起来的缘由是它能够快速的经过调用 eval() 将其转成 javascript 的代码。事实上,Douglas Crockford 本身也在他的开始使用 JSON 的过程当中运用可 ecal() 函数,这也要归功与 eval() 的转换速度。他也确实添加一些判断保证里面真正没有可执行的代码,但实际上这就是基本的 eval()。
目前,为了这个目的,大多数浏览器开始内置了 JSON 的解析功能,尽管仍是有些人仍然采起调用 eval() 函数的办法来获取任意的 javascript 可执行代码,这也是一个延迟加载的策略。对于这一点,一些人认为这才是真正的安全漏洞。若是中间人攻击法再改进一点,那么你将会在页面上执行任意攻击者的代码。
中间人攻击法也经常使用来攻击 eval() 潜在的危险性,打开了安全的缺口。然而这点却不是我所关心的,由于若是你连服务区端返回的数据都不能相信,那么也意味着你不能信任任何东西了,任何东西都有多是坏的。中间人攻击法有不少方法将任意的代码嵌入到你的页面中:
1. 经过加载 javascript 的<script src="">返回攻击者控制代码;
2. 经过 JSON-P 请求返回攻击者控制代码;
3. 经过一个 eval()ed 的 Ajax 请求返回一个攻击者控制代码。
此外,这种攻击能够轻松的窃取用户的 cookies 和用户数据,而不用去修改任何东西。更不用说一些经过钓鱼网站返回的攻击者可控制的 HTML 和 CSS 的可能性。
总之,eval() 并不会使你更容易被中间人攻击法攻击,加载外部的 javascript 标签都会有这个风险。若是你不能信任从服务器返回来的数据,那么无论从哪点上看你的代码有比 eval() 更大的问题存在。
总结
我不是在说你应该在代码中处处开始使用 eval()。事实上,不多有比较的好的用例适合用在 eval() 上,不可忽视的确定会存在的一些问题是代码的清晰度,调试性及代码的性能。可是在一个可以用到 eval() 的场合仍是不要去吝啬(大胆)的使用 eval()。尽可能不要在一开始就使用,但在使用 eval() 的过程当中也不要被任何人说你的代码更加脆弱或是更不安全就吓到。
参考:
- About JSLint by Douglas Crockford (JSLint)
- Eval is evil, Part One by Eric Lippert (Eric’s blog)
- Know Your Engines by David Mandelin (SlideShare)
- eval() usage in my CSS parser by me (GitHub)