如果你还没有与复杂的的安全域(security domain)和应用程序域(application domain)问题打过交道,那么你真是个幸运的家伙。当你在加载外部内容(然后他们开始播放)的时候,默认的设置工作的很好,你甚至不知道他们的存在。
但是某些时候你可能需要控制默认设置以外的更多行为和功能,这样你就会遇到前面所说的问题。你也许会困扰于Security.allowDomain和crossdomain.xml文件的区别,又或者你想要深究关于安全性的最佳实践。如果是这样,那么这篇文章就是你所需要的了。
以下的教程将会讨论什么是安全域和应用程序域,以及他们在Flash Player中应该如何使用。
沙箱是用于区分不同的数据和程序执行。沙箱对于安全性尤其重要。如果没有恰当的信任授权,两个位于不同沙箱内的内容应该没有任何交互。Flash Player的安全模型使用称为安全域的沙箱来分离内容。
虽然安全性是沙箱的主要用途,但这并不是唯一使用沙箱的原因。另外一种可能的情形是使用沙箱来避免命名冲突,这种区分代码的沙箱方式在Flash Player中被称为应用域。
安全域在Flash中是顶级的沙箱。安全域链接到内容的来源域名,或者是被加载的内容(如SWF文件)的来源域名。比如在senocular.com下的SWF文件包含一个链接到senocular.com的安全域,而在example.com下的SWF文件则有一个链接到example.com的安全域。不同的安全域使得SWF文件在Flash Player中播放时运行在自身的沙箱下。
注意:在本教程的例子中,你将看到我用统一顶级域名下的不同子域来代表不同域名,这是因为在Flash中,不同子域和不同顶级域一样,都被视为不同的域。示例中代码也被简化过了,在Flash编辑环境下用时间线代码也可以进行测试。
不可执行的内容(非SWF文件),比如图片或者文本文件,也被划分到安全域中,同样与他们所处的域名相关联。实际上,正是域名决定了这些内容是否能够被某个SWF文件加载。更多这方面的内容将在不可执行文件的信任机制章节中进行讨论。
回到SWF上来,安全域划分了数据和可执行代码。如果两个SWF处于不同的安全域下,某个SWF中的数据(比如某个变量)是不可以被其他SWF获取的,当然,代码也不能执行。如果尝试获取其他域中SWF文件的数据将会产生一个安全错误。
下面的代码展示了一个SWF文件企图加载另外一个SWF,并获取其文档类的实例(也就是主时间线)。
http://same.example.com/parent.swf:
任意想要获取被加载的SWF文件的内容的尝试,甚至包括trace Loader的content属性。都会引起安全错误,因为他们两者处于不同的安全域内。
安全域的划分也适用于Flash Player所使用的原生ActionScript类。Flash Player在每个安全域中都创建了独立的原生类。举例来说,一个安全域内的XML类与另外一个安全域内的XML类是不相同的,改变其中一个XML的静态属性XML.prettyIndent并不会影响到另一个安全域。
下面这个SWF文件加载了两个子SWF,一个来自自身的域,另一个从另外的域加载。我们改变了主文件的prettyIndent属性,来看看这两个子文件的属性输出。
http://same.example.com/parent.swf:
http://same.example.com/child.swf:
http://diff.example.com/child.swf:
可以看到,第二个子文件的属性并没有被改变。这就说明不同的安全域具有不同的原生ActionScript类定义。
尽管安全域只允许相同域下的通讯,但是我们可以通过信任授权来让处于两个不同安全域内的SWF文件进行通讯。通过授权,某个安全域内的文件可以获取另一个域内文件的的数据,或者调用其方法,就像是处于相同的安全域下一样。
在ActionScript中,SWF的信任授权是通过Security.allowDomain(或者类似的Security.allowInsecureDomain)来设置的。在被信任的安全域内的代码可以调用这个方法来授权信任给另一个或者一组在其他安全域内的SWF文件。这种信任是单向的,发起allowDomain的SWF文件不能去访问被授权信任的文件,除非对方也做了信任授权。
在下面的例子中,一个子SWF文件调用allowDomain来允许父SWF的访问:
http://home.example.com/parent.swf:
http://away.example.com/child.swf:
如果没有授信,就像前文说到的,还是会引发安全错误。但是一旦被加载的子SWF调用了allowDomain以后,父SWF文件就可以自由的访问子SWF文件中的内容。要注意的是在这个例子中,由于父SWF文件没有授权away.example.com的信任,所以子SWF仍然无法访问loader的content属性。
信任是非常重要的安全概念,绝对不能掉以轻心。我们经常看见使用通配符的授权:
这么做将允许所有SWF文件,不仅仅只是你加载的或是加载你的,可以通过ActionScript来访问你文件中的数据。就算你没有在文件中包含敏感数据,但是如果你在文件中提供了某些方法去获取这种数据,那也有可能被其他SWF调用。使用allowDomain来授权的信任就像给了其他SWF文件相等的权利:你能做什么,我就能做什么。在接下来的合并安全域章节中你将看到这意味着什么。
如果你只是想让SWF之间能够通信,除了信任授权的方法以外我们还可以使用sharedEvents对象来实现,我们将在在非受信的SWF之间通讯章节中讨论。
由于不可执行文件(也就是非SWF文件)不能调用allowDomain代码,所以这类文件的信任机制在Flash Player中有不一样的处理方法。这就是跨域(cross-domain)策略文件派上用场的地方。
跨域策略文件是一个放在网站的根域名下的命名为crossdomain.xml的XML文件。和allowDomain类似,定义了一组可以被Flash Player加载的安全网站域名。一个简单的跨域策略文件的例子如下:
http://example.com/crossdomain.xml:
你可以从Cross-domain policy file specification (adobe.com)获得详细的文件格式信息。
和allowDomain不同的是,跨域策略文件只提供了包含所有文件(通常是一个域下的所有文件)的用法。上面的例子表示允许来自example.com的任意子域或www.example-partner.com的SWF文件加载example.com下的文件。
由于存在allowDomain机制,跨域策略文件通常不用于授权SWF文件的访问。跨域加载SWF的时候不会请求跨域策略文件。只有当要把一个跨域的SWF合并到当前的安全域的时候,才需要提供跨域策略文件。这个主题将在合并安全域中进行讨论。
不管是标准的位于域名根目录下的跨域策略文件还是用Security.loadPolicyfile指定的跨域策略文件,都只有在需要的时候才会被加载:当内容被加载的同时,跨域策略文件也被一起加载进来。
加载完成后,Flash Player分析跨越策略文件并判断该域是否为信任SWF所处的域。如果答案是肯定的话,文件正常加载,就像处于和SWF文件相同的域一样。反之则有可能有两种情况:
如果文件本身就是数据(文本文件,XML文件,二进制数据等等),那么文件就不会被加载。
需要跨域策略文件来加载仅包含数据的文件
如果文件除了数据以外还有其余用途(图像文件,声音文件等),那么文件还是能够被加载到用户可见(可听)的环境中。比如说图像文件,就算没有跨域策略文件,还是可以在Loader对象中显示给用户看。但是类似BitmapData.draw等直接访问图像数据的方法就不能运行。
需要跨域策略文件来获取其他文件的数据的引用
对此可能大家都有点疑惑。实际上用户是可以访问这些数据的,但是SWF文件不行。Flash Player是在保护用户的数据不被潜藏有危险代码的SWF文件获取。用户不需要关心跨域策略文件也能正常的浏览网页内容。
但这并不是说跨域策略文件可以被忽视,类似于下面这种过分纵容的跨域策略文件有很多潜在的危险:
警告:使用通配符(*)允许所有域的访问等同于:用户可能可以接触到的所有处于该域下的数据都有可能被任意SWF文件获取。
客户端的Flash Player运行在当前用户的认证下,这就表示用户的数据可能就是Flash Player的数据。而且Flash Player的数据可能被任意在里面运行的SWF获取。Flash Player默认只允许相同域名下的SWF的安全数据,并限制跨域SWF的运行。如果没有这层限制,SWF可以获取任意当前用户可以获取的数据。
举个例子:某用户使用他的认证来登录网页的邮件客户端收取邮件,然后用户打开了一个包含有恶意程序的SWF的页面。如果没有跨域限制的话,这个SWF可以用他现有的认证偷偷地加载用户的邮件页面。用户可以访问的内网也不例外,只要用户能去的地方,SWF就能去。幸好Flash Player阻止了这种获取数据的行为,除非该域通过跨域策略文件给予SWF授权。
记住一个原则,永远不要对包含敏感数据的域开发跨域授权,即使需要上面的信息来进行用户认证。把SWF可以访问的数据划分到不同的域或者子域下面。
Domain | Description | Policy file |
---|---|---|
login.example.com | Hosts user data | None |
feed.example.com | Hosts public data | Includes:
|
这将使得敏感数据不可被访问,但是仍然可以对其他域下的SWF文件公开你的其他数据。
如果没有跨域策略文件的信任授权,Flash Player禁止非SWF文件的获取。特别是像文本那样的仅包含数据的文件,甚至不会加载。如果你需要从一个没有跨域授权的域中获取数据,还是有一个变通的办法。
跨域策略文件用于保护数据,特别是保护用户数据,或者说是用户能够接触到的数据。因为Flash Player是运行在客户端的,但是服务器端的代码没有这种限制。服务器完全是另外一台机器,所以用户请求和服务器请求是完全无关的。
由于服务器没有用户的限制,服务器端的代码可以从任意公开的网络服务获取数据。也就是说包含SWF的服务器可以用于访问外部域的数据,然后作为相同域的数据返回给Flash Player。由于处在相同的域下,Flash Player就不需要有跨域策略文件了。
下面的代码演示了一个服务器端的php脚本加载外部数据的例子:
http://home.example.com/loader.swf:
这种解决方案也有一定的问题。首先你必须能够在服务器端部署代码,某些小项目也许根本不需要服务器环境。
另一个可能更重要的原因是这种方式加倍了网络流量。首先必须从外部域加载到你自己的域下,然后才被下载到你的SWF客户端。同时这也加重了你的服务器的负载。使用跨域策略文件的话就不会有这种问题。
这可以作为一种解决方案,但最好还是能用跨域策略文件来解决。
在某些情况下有可能要与其他来源不那么可靠的域中的SWF通讯,你并不希望完全信任该域,放开全部授权。对此LoaderInfo对象的sharedEvents属性提供了另一种机制。sharedEvents对象是唯一的一个可以在不同安全域中发送共享事件的对象。加载者和被加载者都可以通过这个对象来向对方发送事件。
通过sharedEvents对象发送的事件在两个域中都是完全受信的,这就使得在两个安全域中传递的任意数据都无需考虑安全问题。
警告:当心!通过sharedEvents对象传递了错误的数据仍然有可能把你的SWF中的数据暴露出去。比如你的事件包含了一个复杂对象,特别是显示列表上的对象,那么你的整个SWF都将暴露。
所以通过sharedEvents发送的事件应该限制为包含简单数据的事件类型,避免一些被包含后门的SWF程序加以利用。如果你要传递复杂的事件,那要在传递之前先做一下清理。
使用sharedEvents进行通讯的两个SWF需要确保发送和接收的是一致的事件类型。父SWF可以发出一种事件并监听另一种。子SWF可以监听父SWF发出的事件并发出父SWF中正在监听的事件。这些事件的名字可以是随意的。
下面的例子演示了在不同安全域中的父子SWF使用sharedEvents来通讯简单的文本信息的情况。父SWF发出“fromParent”事件,而子SWF发出“fromChild”事件。
http://safe.example.com/parent.swf:
http://untrusted.example.com/child.swf:
任意的事件类都可以像这样用于传递信息,也包括自定义事件。再次强调,要当心不要把包含引用的数据(特别是显示列表上的对象)随着事件一起发送出去。这种情况的例子在场景的拥有者和获取权限章节中可以找到。
如果两个域之间建立了信任关系,一个SWF就能把另外一个SWF加到自己的安全域内,就像是在相同的域下一样。
在这种情况下信任授权的处理有少许不同。首先,包含父SWF的域不需要指定什么,只要执行加载另一个SWF到当前的安全域就表示完全信任这个SWF。
其次,因为子SWF是立即被加载到父SWF的安全域中,并没有机会通过allowDomain进行信任授权声明。当子SWF可以执行allowDomain声明的时候,已经被加载并实例化到另外的域中。所以这种情况下,跨域策略文件将派上用场。实际上这也是跨域策略文件唯一适用于对SWF进行授权的情况。
需要跨域策略文件把跨域的SWF加载到相同的安全域
要加载某个SWF到自己的安全域内,需要给Loader.load方法指定一个LoaderContext对象。LoaderContext对象的securityDomain属性设置为当前的安全域(SecurityDomain.currentDomain)。通过这样的加载方式,父SWF授信给子SWF,而子SWF的授信则需要通过跨域策略文件。
http://host.example.com/parent.swf:
http://trusting.example.com/crossdomain.xml:
http://trusting.example.com/child.swf:
我们可以通过LocalConnection对象的domain属性来检查每个SWF所处的安全域。虽然子SWF原先所处的域是trusting.example.com,但是由于它被加载到父SWF所处的域中,所以子SWF最终所处的安全域是host.example.com。
用这个方式加载的SWF文件权力比用allowDomain授权的更加大。使用allowDomain授权,等同于说你能做什么,我就能做什么。而把SWF加载到同一个安全域,则等同于我能做任何事。在前一种情况下,子SWF只能调用父SWF下的代码,还是受限于父SWF中的定义。但是通过加载到相同的安全域,这些子SWF就可以在你的域下面做任意操作,这包括:
所以在引入跨域SWF文件到你当前的安全域下的时候,你要确保这种权力不会被滥用。
使用包含安全域的LoaderContext对象的load方法不是能够引入跨域SWF到你的安全域的唯一方法。Loader类的另一个方法loadBytes也可以做到。和load不同的是,它不是用URL来加载外部内容,而是直接加载以ByteArray的形式加载对象。
由于ByteArray与域名之间没有关联,所以用loadBytes方法加载的对象将直接进入当前安全域内。因为你在加载包含这些字节对象之前往往都要经过某种信任授权,所以这通常是安全的。
http://host.example.com/parent.swf:
http://trusting.example.com/crossdomain.xml:
http://trusting.example.com/childbytes.swf:
就和前面看到的例子一样,通过检查子SWF文件的LocalConnection.domain属性,使用loadBytes方法加载的子SWF也显示为相同的安全域。
警告:loadBytes方法有个小小的安全问题:可以把授信过的跨域SWF和加载到当前安全域下的SWF两者间的不同扯平。我们知道虽然这两者都是被信任的,但是就像上面的列表中提到的,后者比前者的权力更大。“你能做什么,我就能做什么”与“我能做任何事”之间的差别,结果可以变成没有差别。
这是因为授信过的跨域SWF文件可以访问父SWF的任何对象,包括父SWF对象的Loader实例,一旦拥有了对loadBytes方法的引用,这就意味着可以把某些字节对象加载到当前的安全域。
下面的这个例子展示了这种可能性:
http://good.example.com/parent.swf:
http://evil.example.com/child.swf:
http://evil.example.com/childbytes.swf:
将来版本的Flash Player可能会改变这种行为,所以在程序中不要使用这种方法。我们应该关注的是加载授信过的SWF文件会带来的潜在威胁:暴露你的域下的所有数据。
当第一个SWF文件被加载到Flash Player中的时候,它被加到显示列表的根上,也就是我们所说的stage对象。这也是Flash Player自己的显示对象的根。每个SWF都有代表自己主文档类或者主时间线的根(叫做root)。第一个被创建的SWF实例的根被放置于场景上,其他子SWF使用Loader对象的实例来加载。
场景的特别之处在于它本身就位于显示列表上,所有处于显示列表上的子SWF都可以取得它的引用,但是它只有一个拥有者:就是第一个被实例化的那个SWF。场景的拥有者决定了场景所连接的安全域。其他的SWF想对场景进行特殊操作的行为都必须获得场景所有者的信任授权。
你可能有注意到在过去的有些程序或者是组件中,被加载到不同的域(未授信)里的时候报错。这正是因为没有取得对场景对象进行操作的授权。因为场景对象是可以被引用的,但是诸如场景的addEventListener方法等却不可用,所以这很容易引起误解。
下面这个表格列出了场景对象限制非安全域对象访问的成员。可能不是100%精确,主要用于参考。
addChild | addChildAt | removeChild |
removeChildAt | getChildIndex | setChildIndex |
getChildAt | getObjectsUnderPoint | swapChildren |
swapChildrenAt | numChildren | tabChildren |
mouseChildren | width | stageWidth |
fullScreenWidth | height | stageHeight |
fullScreenHeight | quality | align |
scaleMode | displayState | fullScreenSourceRect |
stageFocusRect | showDefaultContextMenu | colorCorrection |
addEventListener | dispatchEvent | hasEventListener |
willTrigger |
在下面的例子中看看场景是如何可以被子SWF访问,但是却不能调用stage.addEventListener方法。
http://first.example.com/parent.swf:
http://second.example.com/child.swf:
场景的这种所有者关系非常操蛋,因为我们经常需要对场景对象监听鼠标或者键盘事件,比如检测键盘按下或者检测鼠标在物体外部释放点击。在这种情况下,单靠子SWF自身是没办法完成的。还好,场景拥有者的父SWF可以通过sharedEvents传递场景事件而不必授信给子SWF。通过这种方式,可以在保护主域的前提下配合完成这种工作。
警告:以下这个使用范例演示了sharedEvents是如何处理安全性问题的。一些鼠标事件的relatedObject属性持有对时间线上的对象的引用。如果不经过清理,这些对象就会暴露给没有经过授信的域。所以通过sharedEvents发送事件时,要把这些引用清除。比如MouseEvent,我们可以新建一个仅包含必须数据的MouseEvent对象。
下面的示例展示了如何通过sahredEvents发送场景事件。示例中仅仅转发了MOUSE_OUT事件,当然也可以扩展于其他的事件类型。注意如何创建一个代理事件并保护父SWF中的对象。
http://stageowner.example.com/parent.swf:
http://untrusted.example.com/child.swf:
幸好有safeEvent这个MouseEvent的实例,原本的MouseEvent对象的relatedObject指向的引用被屏蔽了。实际上,所有通过sharedEvents对象发送的事件都应该用这种方式清理一遍。
在硬盘上运行的SWF文件同样有自己的安全域。本地安全域有自己独特的行为,共分为4种安全沙箱类型:local-with-file, local-with-network, local-trusted, and application for AIR(本文不详细讨论AIR)。再加上网络上的SWF,一共有5种安全沙箱。在ActionScript中,你可以用Security.sandboxType来获得当前的安全沙箱类型。
Security.LOCAL_WITH_FILE
)—本地不受信任的文件,只可以访问本地数据,不能与网络通信。Security.LOCAL_WITH_NETWORK
)—本地不受信任的文件,只可以访问网络,但是不能读取本地数据。Security.LOCAL_TRUSTED
)—本地受信的文件,通过Flash Player设置管理器或者FlashPlayerTrust文件授权。可以访问本地和网络。Security.APPLICATION
)—随着AIR包而安装,运行在AIR程序中。默认在这种沙箱类型下的文件可以调用任意域下的文件(外部域可能不允许)。而且默认能从任意其他域下加载数据。Security.REMOTE
)—来自网络的文件,遵循安全域的沙箱规则。由于有可能从用户的硬盘上获取敏感数据,所以本地文件在安全方面有着严格的规则。一旦有机会,恶意的SWF将可以从电脑上读取数据并上传到网络上的服务器。所以为了防止这种情况,本地的SWF只允许一种类型的通讯,要么就是本地,要么就是网络。而且,不同安全沙箱类型的SWF不能相互加载,避免能同时访问网络和访问本地的情况出现。
由于我们不能判断本地文件的域名,所以判断本地SWF文件的安全沙箱用的是另外一种形式。只允许本地和只允许网络这两种情况是通过SWF文件内的标识来区分的,所有的SWF在发布的时候都带有这种标识,当SWF在本地运行的时候,Flash Player就用它来检测安全沙箱类型。
对于本地的信任文件,相同的问题是我们没有地方来保存跨域策略文件,所以我们通过Flash Player的设置管理器里的全局安全设置面板来设置。通过下图中所示的下拉菜单把本地SWF的路径添加到本地的安全文件列表里。
Flash Player 设置管理器
另一种方式是通过配置文件的方式。配置文件不像设置管理器那样需要联网来使用。要把SWF或者包含SWF的文件夹添加到信任位置的话只需要添加路径到Flash Player的#Security\FlashPlayerTrust下的.cfg文件就可以了。在Mac和Windows上,这个路径如下:
- Windows 95-XP:
C:\Documents and Settings\[username]\Application Data\Macromedia\Flash Player\#Security\FlashPlayerTrust- Windows Vista-7:
C:\Users\[username]\AppData\Roaming\Macromedia\Flash Player\#Security\FlashPlayerTrust- Mac OS X:
/Users/[username]/Library/Preferences/Macromedia/Flash Player/#Security/FlashPlayerTrust
把[username] 替换为你的用户名。
两种方法都是把信任授权信息写入你的硬盘(全局安全设置面板也一样,保存在Flash Player的特定目录下)。这些文件就像你本地的跨域策略文件一样。当Flash Player读取的时候,信任授权给SWF,并覆盖SWF文件中关于本地访问权限的标识,从而允许受信的SWF能够同时访问本地和网络。
关于本地信任机制还有一点需要注意的是即使是本地受信的文件,仍然不能把处于外部沙箱的内容加载入本地沙箱。
大多数Flash内容都是为网络创建的,所以通常不需要完全理解本地的安全沙箱机制。但是开发和测试是一个例外。Flash编辑器在测试的时候就是把SWF放在一个本地受信的安全沙箱里面。这有可能会造成与真正发布的SWF情况不同的结果,因为两者的安全沙箱不同。比如你测试的时候可以从没有授信的外部域读取内容,而真正发布到网站上的SWF却无法加载。所以当你测试的时候要注意,你所看到的结果和最终发布的版本有可能不一样。
你可以到Flash Player Developer Center(adobe.com)的Security section查看更详细的安全相关信息。