WEB安全第八课 浏览器端脚本 之二 JavaScript的基本特点 |
JavaScript是一门相当简单的运行时解释语言。它的语法隐约受到C语法的影响(除了没有指针运算);对象模型很直接也没有类的概念,这点据说是受到鲜为人知的Self编程语言的影响;有自动垃圾回收;以及弱数据类型和动态类型等特点。
JavaScript也没有内置的I/O机制。JavaScript程序与宿主环境进行交互,是通过一系列预定义的方法和属性实现的,这些方法和属性会再映射成浏览器的内部原生代码,所以与其他很多常规的编程语言不同,浏览器开放的这些接口往往非常受限且有针对性。 对已有C、C++甚至Java经验的程序员来说,JavaScript的大部分核心特点都平淡无奇。一个简单的JavaScript程序如下所示:👨🦱💍💰😥👊 [mw_shl_code=javascript,true] var text= "Hi mom!"; function display_string(str){ alert(str); return 0; 👂🎢🥛📶🦦 }//这里会显"Hi mom!” dlsplay_str(text);[/mw_shl_code] 本贴就不提供更详细的JavaScript语义介绍了,会侧重于介绍JavaScript的一些最重要的特点以及与安全相关的内容。如果读者希望系统地了解这门语言,可以选择阅读Marijn Haverbeke所著的《Eloquent JavaScript》(No Starch Press出版社,2011年出版,中文版名为《JavaScript精解》,书号为978-7-111-39665。) ✋🏦🍟🅿🐟 1.脚本处理模型 无论是在独立窗口或框架里,每个展示在浏览器里的HTML文档,都被赋予了一个独立的JavaScript执行环境实例,在这个环境里加载的脚本的所有全局变量和函数都拥有一个独立的命名空间。同一个文档的所有脚本都运行在同一个执行环境里,共享同一个沙箱,并能够通过浏览器提供的API与其他上下文环境交互。这种跨文档的交互必须以非常显式的方式进行;不可能在无意中产生相互干扰。从表面上看,脚本隔离的规则是继承自现代多任务操作系统里的进程隔离,但脚本隔离涉及的范围远比进程隔离小。 👨🦱🎒✒😰👌 在特定的执行上下文里,每段JavaScript代码块都是自成体系处理的,顺序也基本确定。每段代码块都由若干符合语法格式的独立单元组成,处理的过程包括清晰且连续的3个步骤:源码处理、函数解析和代码执行。 1.1源码处理 源码处理阶段会检查脚本代码块里的语法,通常会先把代码转换为中间层的二进制映像,这样才能获得相对令人满意的执行速度。在彻底完成这一步骤之前,这些二进制代码对全局并无影响。如果源码处理阶段出错,整段有问题的代码块都会被弃用;然后解析器会继续处理下一段代码块。 🤛🛩🥩♏🦖 我们用以下HTML代码片断,展示符合规范的JavaScript解析器的处理: [mw_shl_code=javascript,true] 区块 #1: <script> var my_variable1 = 1;👩🩰🖥🙄🙏 var my_variable2 = </script> 区块#2: <script> 2; 👩✈️🥾📬💩🖐 </script>[/mw_shl_code] 和程序员们学过的c处理方式不同,上述代码并不等于以下效果: [mw_shl_code=javascript,true] <script> 👃🌞🍖❌🐯 var my_variable1=1; var my_variable2=2; </script> [/mw_shl_code] 👨🚒👔😳👍 这是因为<script>代码块在解析之前,还不会被拼接到一起。实际上,第一段代码会引起格式错误(赋值时缺少了右边的具体数值),导致整个代码块被忽略,所以也不会到执行阶段。由于整个代码块在能产生任何作用之前已被弃用,这也意味着我们的例子也不会达成以下效果: [mw_shl_code=javascript,true] <script> var my_variable1=1; </script>👳🪖⌨😉🖕 <script> 2; </script>[/mw_shl_code] ✋🗽🍭ℹ🐢 在这点上JavaScript有别于任何其他脚本语言(如Bash),其他语言的格式解析和执行阶段不会那么严格地被分隔开来。 所以我们在本节最开始时列举的那个例子的实际效果为,第一部分的代码完全被忽略,而第二部分(<script>2;</script>)则被正常解析。在运行时第二部分代码实际上没有执行任何操作,因为只有一个纯数字型的代码语句。 1.2函数解析 🦴🚈🍞↔🦖 顺利完成源码处理的步骤后,下一步就是解析器对当前代码块里所有具名的全局函数进行识别并注册。在这一阶段完成后,这些函数才能被执行代码所调用。 因为JavaScript在执行前的这种额外预处理,因此以下写法能成功执行(可能与C或C++程序员熟悉的处理不同,由于对hello_world()函数的注册要先于第一行代码的执行,所以这行代码可以成功调用hello_world()函数): 🎩🎷😆🤝 [mw_shl_code=javascript,true] <script> hello_world(); function hello_world(){ alert('Hi mom!'); 🦴🚗🥑📳🦠 } </script>[/mw_shl_code] 而另一方面,这个略做修改的版本却会有完全不同的效果: 🦷⛵🥩✡🐺[mw_shl_code=javascript,true] <script> hello_world(); </script> <script> 🧑🌾👠📷😶🖕 function hello_world(){ alert('Hi mom!'); } </script>[/mw_shl_code] 👌🏦🔪🈸🐟 这个修改版会因为运行时错误而执行失败,因为代码里每段独立的代码块并不是同时处理的,这是根据JavaScript引擎读取代码块的先后顺序决定的。在执行第一个代码块时,定义hello_world()的那个代码块还未被解析呢。 好像还嫌问题不够复杂似的,JavaScript这种有点笨拙的全局名称解析模型只对函数有效,而对变量的声明却并非如此。和其他脚本语言相类似,变量是按执行时出现的顺序注册的。以下代码例子仅仅是把原来的全局函数hello_world()以匿名函数的方式指定给一个全局变量,却无法按预期的结果执行: [mw_shl_code=javascript,true] <script> 👃💈🥚🆘🦠 hello_world(); var hello_world = function(){ alert(’Hi mom!'); } </script>[/mw_shl_code] 👍🚂🫖♏🦠 因为这个例子在执行hello_world()调用时,对hello_world变量的赋值还没有开始呢。 1.3代码执行 💅🏫🍍❎🐮 一旦函数解析阶段也执行完毕,JavaScript引擎就开始按顺序执行在函数区块之外的所有代码。如果在执行过程中,由于某些未处理的异常或一些更偏门的原因,脚本的执行可能会失败。如果碰到这种错误,那些已经被正确解析的函数仍能被调用,而已经执行过的代码所产生的结果,对此上下文环境也仍然继续有效。 以下这个代码片段稍有些长但颇有趣,其中就展示了JavaScript的异常恢复处理和其他一些运行特点: ![]() ![]() 🤳🏦🌶🆚🐖 请自行分析这个例子,看是否认可右边标注里的运行结果。 从这个练习可以清楚地看到,由于出现了未预期和未处理的某些异常情况,所以程序的运行会带来一些出人意料的后果:此时整个应用的状态会变得不太统一,实际上即使有异常,代码也仍然有可能继续执行。异常的本意是阻止未能预期的错误,不会再扩大这种错误的影响,所以JavaScript这么设计颇为古怪——特别是考虑到在其他许多方面(例如禁用goto语句),JavaScript的表现颇为古板。 2.执行顺序的控制👦🪦😳👃 为了正确分析常见Web应用设计模式的安全属性,我们有必要了解JavaScript执行引擎的执行顺序和时序模式。谢天谢地,这套模式相当合理。 实际上,在同一个执行环境里的JavaScript是按时间顺序运行的。当JavaScript代码还在运行时,外部事件无法中断代码的运行,同时也不支持线程对任何共享内存的修改。当执行引擎在忙的时候,事件处理、定时器、页面浏览的请求等动作也都会往后延;在大多数情况下,整个浏览器里至少HTML渲染器部分基本处于不响应状态。只有当代码执行结束后,脚本引擎恢复空闲状态,才会重启对排队事件的处理。此时才有可能重新执行JavaScript代码。 👏🎠🥚🈴🐤 更进一步说,JavaScript本身并没有sleep(...)或pause(...)这样的暂停功能来临时释放CPU,或在暂停结束后从同一位置重新开始执行。JavaScript的做法是,如果程序员希望推延某个脚本的执行,需要先注册一个定时器(Timer),以便在延迟一段时间后能启动一个新的执行流。只有在触发了特定的处理器函数后,才会开始这个执行流。(设置定时器时,还可以直接在处理器函数里内嵌一段完整JavaScript代码。)尽管这种设计理念用起来有点麻烦,但能减少代码运行结果里出现竞争条件(RaceCondition)的风险。 由于这段文字描述比较含糊,再略加一点补充。这段提到可以有两种写法来设置某个事件处理程序,譬如,需要对某个表单按钮设置一个独立的计时函数,第一种方法是onclick="setTimeout('someFunction(...)',1000)";但也可以在onclick的方法里,就直接写一段内嵌式的JavaScript函数:onclick="(function(){setTimeout(function(){/*这里是一段JS代码段*/},1000);})();",两者具有同样的效果。 👩✈️💍🔋🥰👍 注意:在这套同步执行模式里可能有些无意中产生的漏洞。其中一种情况是在调用类似alert(...)或showModalDialog(...)这些函数时,如果它们处于临时挂起的状态,系统其实仍然有可能执行其他的JavaScript代码。当然这种极端的情况确实比较少见。 繁忙的JavaScript循环可能引起浏览器被挂死这样的破坏性后果,所以在浏览器里需要有一些能减缓此类问题影响的处理。我们会在后面进一步讨论细节。现在来说,只需要知道JavaScript有一个异乎寻常的处理就足矣:实际上,任何死循环都能被中断退出,后果等价于一个未处理的异常。循环退出,引擎恢复到闲置状态,引起问题的代码也仍然可以被调用,所有的计时器和事件句柄也会保持原样。 由于CPU占用率特别高的代码执行能被中断,所以如果攻击者刻意这么做,就能导致程序退出,使应用处于完全不确定的状态,而非开发者原先预想的那样成功执行完成。还远不止这一种后果,还有一个与语义处理紧密相关的问题,会在下面讲到。 🤝🛩🫑🐤 3.代码和对象检视功能 对于那些非JavaScript内置的原生函数,JavaScript语言提供了一个基本的功能,可以查看这些函数经过反编泽后的源代码,幵发人员只要调用toString()方法,就能方便地检视他们感兴趣的函数。但除了这一功能,检查程序运行流的机会非常有限。应用勉强能访问到宿主页面的内存映像,所以能直接检索在本页面里的<script>代码块内容,但对远程加载的JavaScript文件内容或动态产生的JavaScript代码就没有什么直接可用的办法了。 🧠🎢🥛🈷🦌 某些情况下可以通过非标准的caller属性来返回调用堆栈的信息,但同样也没什么办法能知道当前执行或将要执行到哪一行代码。 可以动态创建新的JavaScript代码是这门语言另一项显著特性。它可以通过内置的eval(...)函数,让解析引擎顺序解析传进去的字符串。例如,以下例子会弹出一个警告窗口: [mw_shl_code=javascript,true] eval("alert(\"Hi mom!\")")[/mw_shl_code]👨⚕️👠📥🤬🤳 如果eval(...)收到的输入文本有语法错误,这个函数就会拋出异常。同样,语法解析无误后,在代码执行过程中产生的未处理的异常也会传递到调用eval的那个函数里。最后,如果语法没有错误,运行也没问题,这段代码在JavaScript引擎里执行之后,最后一行代码的执行结果就会被视为整个eval(...)函数的返回值。 和eval(...)函数类似,还有一些别的浏览器机制会等到解析引擎处于闲置状态时,再对新的JavaScript代码块进行延迟解析执行。这类机制包括定时器(setTimeout、setlnterval)、事件处理器(onclick、onload等)和HTML解析器自身的若干接口(innerHTML、document.write(...)等)。 🖕🚘🍪☣🦉 如果说JavaScript检视代码的功能还有点不上台面,那它的实时对象自省能力可算非常完备了。不但可以使用简单的迭代器如for...in或for each...,还可以通过操作符如typeof、instanceof或“全等符”(===)和类似length这样的属性,来获知每个元素的额外信息。 上述这些特性使得运行在同一上下文环境里的脚本很难彼此隐瞒。这项功能甚至使得跨页面的上下文环境之间要保持秘密也有难度,这个问题已经长期困扰着浏览器开发商们——等读到后面,你会发现这事还没完呢。 4.修改运行环境🧢📐😪👄 尽管JavaScript语言相对简单,但执行脚本还是有许多不同寻常的方式能干预自身的JavaScript沙箱。在一些罕见的例子里,这些行为还会进一步影响到其他页面。 4.1重写内置函数 👆🛑🥭ℹ🐉 如果任由流氓脚本发挥,可做的坏事包括删除、重写或屏蔽大部分的JavaScript内置函数和所有由浏览器提供的I/O方法。例如,考虑一下以下代码的结果: [mw_shl_code=javascript,true] //这个赋值不会引起什么错误 eval = alert; 🧓🩲🖲🤪🖕 //这个调用却会弹出一个对话窗口 eval("Hi mom!");[/mw_shl_code] 好玩的事情才刚开始!在执行了上述代码之后,Chrome、Safari和Opera浏览器里都可以通过delete操作符,彻底删除整个eval(...)函数。但容易引起混乱的是,如果在Firefox里也这么做,却会恢复成原来的内置函数,重写的效果被清除了。如果在IE浏览器里,这种删除操作却会引起一个滞后的异常报错,等它发生时貌似已没什么实际意义了。 ![]() 🧒🧢🛏👻🖕 从这些代码延伸开去,几乎所有的对象,包括内置对象如String或Array,都有一个能被随意修改的原型。这个原型是个主体(master)对象,已产生的全体对象实例甚至包括还未产生的实例,方法和属性都衍生自这一主体(与更完整编程体系里的类继承意思大体相当)。因为对象原型能被篡改,导致新建对象的行为可能会相当不符合常理,如下所示: [mw_shl_code=javascript,true] Number.prototype.toString = function(){ return "Gotcha!"; }; 🧓🧢🧹🤪👍 //以下这行代码会显示“Gotcha!”而不是“42"; alert(new Number(42));[/mw_shl_code] 👴👔📷😭🖐 4.2 Setter和Getter 现在使用的JavaScript语法里更为有趣的对象模型特性是setter和getter:自定义代码可以通过这两个接口读取或设置主对象的属性。尽管不如C++语言里的运算符重载那么强大,但这两个接口仍有可能使已有对象或对象原型的行为变得更令人困惑。在以下代码片段里,无论是设置还是读取对象的属性确实都变得非常简单: [mw_shl_code=javascript,true] var evil_object={ 🩴📮😈👎 set foo() { alert("Gotcha!");}, get foo() {return 2; } }; //以下这行代码只会显示“Gotcha!”而不会产生别的效果。👵🛍🧹🥱🙏 evil_object.foo=l; //而这行比较的结果则是两者不相等。 if(evil_object.foo!=1) alert("What's going on?!");[/mw_shl_code] 🧑🚀💄🩺😄👁 注意:setter和getter最开始只是浏览器厂商自己的扩展,但现在已经在ECMAScript第5版里成为新标准。现在的常用浏览器里,除了IE6和IE7不支持外,其余所有浏览器都支持这一特性。 4.3 JavaScript使用的潜在风险 从前的技术讨论可知,在特定上下文环境里的执行脚本,一旦受到非信任内容的干扰,就没有什么可靠的办法来检查其运行环境是否正确,也无法采取什么有效的措施进行纠正了;即便是一个简单的条件判断或循环也未必可靠。针对JavaScript语言的一些增强型提议可能也会使它变得更复杂。例子可参见ECMAScript第4版里未获得支持的全面的操作符重载提议,而这项提议仍有可能死灰复燃呢。👩✈️👑💉🤖🤞 更有意思的是,这种设计理念使得检查每个页面沙箱之外的执行环境变得非常困难。例如,如果盲目信任潜在恶意页面的location对象,则有可能会导致一系列与浏览器插件、和JavaScript相关的扩展、客户端Web应用安全机制有关的漏洞。最后厂商们不得不在浏览器里专门针对location这个对象,又再做了一些额外的特殊防护,但实际上,整个对象层级里的其他部分还是门户洞开的状态。 5.JavaScript对象表示法(JSON)和其他数据序列化 👵👠🧯☠🤌 JavaScript—项重要的语法结构就是其简洁方便的“就地”(In-place)对象序列化处理,也叫JavaScript Object Notation(JavaScript对象表示法),或简称JSON(参见RFC4627)。这种数据格式通过重写大括号({)操作符的含义来实现。这与碰到的是符合规范的JavaScript语句(Statement)时,用法类似,在碰到语句的情况时,大括号代表着一段嵌入代码块的起始。但是,当碰到的是表达式(Expression)时,大括号的含义就代表着一个序列化对象的开始。以下例子展示了在表达式语法里正确使用大括号,其运行结果是弹出一个简单的提示: [mw_shl_code=javascript,true] var impromptu_object = { "given_name":"John", "family_name":"Smith",🧑🎤🩳⚔🤖🧠 "lucky_numbers":[11630,12067,12407,12887] }; //以下一行代码会提示"John". alert(impromptu_object.given_name);[/mw_shl_code]👳💍📐😇👊 JSON的对象序列化与之前那种显式的,通过数字、字符串、数组的序列化明显不同,由于对大括号的含义进行了重载,这意味着JSON区块里的内容不能按照独立的代码语句来执行。这条限制乍看似乎无足轻重,但有个好处:这种机制使得任何来自服务器端,内容符合JSON语法的响应内容,都不能以<script src=...>方式进行跨站引用。所以以下例子会引发语法错误,表面上是由于解析器认为JavaScript代码标号(code label)中含有非法的引号(❶)导致的,于是这样就不会带来什么显著的副作用了: ![]() 注意:不能用<script src=...>方式加载JSON是一项有意思的特性,但这并非完全可靠。特别是,如果用小括号或中括号把响应的数据括起来,或有可能移除代码标号两边的引号时,返回的JSON数据就会变成在独立区块里可以正常运行的代码了,这样会带来明显的副作用。 🖕🚘®🪶 由于JavaScript语法的发展更新实在太迅猛,如果日后还依赖于JSON的写法必定会引起JavaScript解析错误,就认为可以一直高枕无忧实在不是明智之举。当然,对许多非关键性应用来说,把JSON作为一种简单安全机制已经足够了。 使用诸如XMLHttpRequest 的方式获得JSON数据后,在各常见浏览器里通过JSON.parse(...)函数执行快速序列化,就能毫不费力地把这些数据转化成内存里的对象——但IE浏览器却不在此列。不幸的是,为了与IE浏览器兼容,又或出于习惯问题,很多开发人员转而使用同样快速但更危险的做法: 👂🏝🥛♻🐡 [mw_shl_code=javascript,true] var parsed_object=eval("(" + json_text + ")");[/mw_shl_code] 这条语法的问题是用eval(...)函数去计算JSON表达式的“值”,因为eval不但能用于对纯JSON输入进行序列化,还可以执行符合规范的JavaScript语句。这可能导致谁也不希望看到的全局副作用。例如,内置在这个假造的JSON响应里的函数调用就能获得执行: [mw_shl_code=javascript,true] {"given_name":alert("Hi mom!")} 👨🚒👑🦯👈 [/mw_shl_code] 这种行为会增加前端开发人员的额外负担,因为需要判断只接受来自可信任源的JSON,并对自身服务器代码产生的推送也要做恰当的转义处理。很容易预见到,如果不这么做会产生不少应用级别的安全漏洞。 注意:eval(...)执行结果的不可信,部分原因要归咎于JSON规范(RFC4627)自身:这个文档里提供了一个据说是安全的解析器具体实现,在JSON数据未被引号括起来的情况下,它允许JSON里包含仅由“Eaeflnr-u”这些字符和数字组成的若干程序变量,但这已经足够拼出“unsafe”(不安全)这个单词以及约1000个常用的英文单词,实际上已不太安全了。这条被RFC认可但实际上大有问题的正则表达式在互联网上随处可见,而且显然还会持续下去。 👃🔥🌰🈸🦬 由于JSON的简单易用,所以在各种常见Web应用的服务器端-客户端通信方式里得到了普遍支持。与之竞争的只有若干其他较不安全的字符串或数组序列化方式和JSONP。然而这些其他的序列化方式也都无法兼容JSON.parse(...)的用法,必须使用不安全的eval(...)才可以把数据转换到内存里。 这些格式的另一个特性是,有别于正式的JSON,通过第三方页面以<script src=...>形式加载这些数据时,解析不会出现错误。这一特性只有在一些非常罕见的情况里才算是优势,而大多数情况下这只会带来隐蔽的安全风险。例如,通过<script>标签加载数组形式的序列化数据时,原本没有什么副作用,但经过攻击者最近的改进后,至少可以通过修改数组对象原型的Setter,得以窃取返回的数据。并非特别管用的一个常见做法是在响应数据的开头部分添加一个while(l);死循环来防范这种攻击,但这也会导致一些有意思的副作用,还记得之前提过的JavaScript死循环中断问题吗? 6.E4X和其他语法扩展🩰🔍😆👀 和HTML类似,JavaScript的发展非常迅猛。近几年对它的修改也相当激进,这可能导致一些以往被解析器拒绝的写法现在有可能会变成合法的JavaScript代码。这可能会导致难以预期的数据泄漏,尤其是结合本章前面提到的丰富的代码和对象检视及修改特性,以及通过<script src=...>加载跨域代码的做法。 这种趋势里最值得关注的一个例子是XML格式的ECMAScript(ECMAScript for XML,E4X),这是在JSON序列化之外另一种把XML语法整合到JavaScript里的做法,尽管优雅但完全没必要。在任何E4X兼容引擎(如Firefox)里,下面两段代码的含义差不多完全—样: 👂🌞🍼🐺 [mw_shl_code=javascript,true] //常规的对象序列化var my_object = { "user":{ "given_name":"John", "family_name":"Smith", "id":make_up_value() 💪💈🍇⁉🐥 }};//E4X序列化 var my_object = <user> 🥷🥼📐🤪🦷 <given_name>John</given_name> <family_name>Smith</family_name> <id> { make_up_value() } </id> </user>;[/mw_shl_code] ✌🏠🍖✡🐞 在这种情况下,原本只是符合规范的XML文档,却突然变成能通过<script src=...>形式加载的代码,也就是突然从表达式变成了语句(expression-as-statement),所以就可能导致出人意料的执行结果。而如果攻击者能技巧地在被包含页面两边加上"{"和"}"字符,或改变对象原型的Setter定义,就有可能窃取用户的相关信息,并在无关的页面里展示出来。以下例子就描绘了此类风险: ![]() 这里要表扬一下Firefox的开发人员,在忍受了这个问题几年之后,他们终于下定决心禁止解析脚本里的任何E4X语句,部分杜绝了这一问题。然而,由于JavaScrip这门语言非常灵活多变,所以用JSON响应解决跨域脚本包含的问题是否足够可靠依然令人起疑。如果日后"{"符号有了新的第三种含义,或代码标号的写法里允许使用引号字符时,那这类服务器到客户端数据交换格式的安全性就会大打折扣。对此类问题宜未雨绸缪。
帖子热度 9745 ℃
|
|