跨脚本访问不同安全沙箱中的内容

Adobe AIR 1.0 和更高版本

运行时安全模型将代码与不同的源隔离开来。通过跨脚本访问不同安全沙箱中的内容,可允许一个安全沙箱中的内容访问另一个沙箱中的所选属性和方法。

AIR 安全沙箱和 JavaScript 代码

AIR 强制实施同源策略,以防止一个域中的代码与另一个域中的内容进行交互。所有文件根据其来源放置在相应的沙箱中。通常,应用程序沙箱中的内容不能违反同源原则,并且不能跨脚本访问从应用程序安装目录外部加载的内容。但是,AIR 提供了一些方法,可让您跨脚本访问非应用程序内容。

一种方法是使用 frame 或 iframe 将应用程序内容映射到其他安全沙箱。从应用程序的沙箱区域加载的任何页的行为与从远程域加载该页的行为相同。例如,通过将应用程序内容映射到 example.com 域,该内容可以跨脚本访问从 example.com 加载的页。

由于此方法将应用程序内容放置到其他沙箱中,因此该内容中的代码也不再受计算出的字符串中对代码执行的限制。即使不需要跨脚本访问远程内容,也可以使用这种沙箱映射方法来减弱这些限制。当使用多个 JavaScript 框架中的一个框架或者使用依赖于计算字符串的现有代码时,采用此方法映射内容特别有用。但是,应考虑并防止运行应用程序沙箱以外的内容时可能插入和执行不可信内容的额外风险。

同时,映射到其他沙箱的应用程序内容将失去访问 AIR API 的权利,因此沙箱映射方法不能用于向在应用程序沙箱外部执行的代码公开 AIR 功能。

另一种跨脚本访问的方法,是在非应用程序沙箱中的内容与其在应用程序沙箱中的父级文档之间创建一个名为 沙箱桥 的接口。沙箱桥允许子级内容访问父级内容所定义的属性和方法,或允许父级内容访问子级内容所定义的属性和方法,或者两者同时允许。

最后,还可以从应用程序沙箱和其他沙箱(可选)执行跨域 XMLHttpRequest。

有关详细信息,请参阅 HTML frame 和 iframe 元素 Adobe AIR 中的 HTML 安全性 XMLHttpRequest 对象

将应用程序内容加载到非应用程序沙箱

若要允许应用程序内容安全地跨脚本访问从应用程序安装目录外部加载的内容,可以使用 frame iframe 元素将应用程序内容加载到与外部内容相同的安全沙箱。如果不需要跨脚本访问远程内容,但仍希望加载应用程序沙箱外部的应用程序页,则可以使用同一方法,指定 http://localhost/ 或某些无不利影响的其他值作为源域。

AIR 将向 frame 元素添加新的 sandboxRoot documentRoot 属性,以便允许您指定是否应将加载到该框架中的应用程序文件映射到非应用程序沙箱。对于解析为 sandboxRoot URL 之下的路径的文件,将改为从 documentRoot 目录加载。出于安全方面的考虑,使用此方法加载的应用程序内容将被视为是从 sandboxRoot URL 实际加载的。

sandboxRoot 属性指定用于确定放置框架内容的沙箱和域的 URL。必须使用 file: http: https: URL 方案。如果您指定的是相对 URL,则内容将保留在应用程序沙箱中。

documentRoot 属性指定从中加载框架内容的目录。必须使用 file: app: app-storage: URL 方案。

以下示例对要在远程沙箱中运行的应用程序的 sandbox 子目录中安装的内容以及 www.example.com 域进行了映射:

<iframe 
    src="http://www.example.com/local/ui.html"  
    sandboxRoot="http://www.example.com/local/"  
    documentRoot="app:/sandbox/"> 
</iframe>

ui.html 页可以使用以下脚本标签从本地的 sandbox 文件夹加载 javascript 文件:

<script src="http://www.example.com/local/ui.js"></script>

它还可以使用以下脚本标签从远程服务器的目录加载内容:

<script src="http://www.example.com/remote/remote.js"></script>

sandboxRoot URL 将遮盖远程服务器上位于相同 URL 中的所有内容。在上例中,您不能访问位于 www.example.com/local/ (或其子目录中)中的任何远程内容,因为 AIR 会将请求重新映射到本地应用程序目录:无论是页面导航、XMLHttpRequest 还是采用其他内容加载手段所派生的请求,都会重新映射。

设置沙箱桥接口

如果应用程序沙箱中的内容必须访问非应用程序沙箱中的内容所定义的属性或方法,或者如果非应用程序内容必须访问应用程序沙箱中的内容所定义的属性或方法,则可以使用沙箱桥。使用任何子级文档的 window 对象的 childSandboxBridge parentSandboxBridge 属性可创建沙箱桥。

建立子级沙箱桥

childSandboxBridge 属性允许子级文档向父级文档中的内容公开接口。若要公开接口,需将 childSandbox 属性设置为子级文档中的函数或对象。然后,可以从父级文档中的内容访问该对象或函数。以下示例显示了子级文档中运行的脚本如何向其父级文档公开包含函数和属性的对象:

var interface = {}; 
interface.calculatePrice = function(){ 
    return ".45 cents"; 
} 
interface.storeID = "abc" 
window.childSandboxBridge = interface;

如果此子级内容加载到 iframe(分配的 ID 为“child”),则可以通过读取 frame 的 childSandboxBridge 属性从父级内容来访问接口:

var childInterface = document.getElementById("child").contentWindow.childSandboxBridge; 
air.trace(childInterface.calculatePrice()); //traces ".45 cents" 
air.trace(childInterface.storeID)); //traces "abc"

建立父级沙箱桥

parentSandboxBridge 属性允许父级文档向子级文档中的内容公开接口。若要公开接口,父级文档需将子级文档的 parentSandbox 属性设置为父级文档中定义的函数或对象。然后,可以从子级文档中的内容访问该对象或函数。以下示例显示了父级 frame 中运行的脚本如何向其子级文档公开包含函数的对象:

var interface = {}; 
interface.save = function(text){ 
    var saveFile = air.File("app-storage:/save.txt"); 
    //write text to file 
} 
document.getElementById("child").contentWindow.parentSandboxBridge = interface;

使用此接口,子级 frame 中的内容可以将文本保存到名为 save.txt 的文件,但对文件系统不具备任何其他访问权利。子级内容可以调用 save 函数,如下所示:

var textToSave = "A string."; 
window.parentSandboxBridge.save(textToSave);

应用程序内容向其他沙箱公开的接口应越窄越好。应考虑到非应用程序内容本身并不可靠,因为它可能遭到意外或恶意代码注入。必须采取适当的防护措施,以防止误用通过父级沙箱桥公开的接口。

在页面加载过程中访问父级沙箱桥

为了使子级文档中的脚本能够访问父级沙箱桥,必须先设置沙箱桥,然后才能运行脚本。创建新页 DOM 之后,在分析任何脚本或添加 DOM 元素之前,Window、frame 和 iframe 对象将调度 dominitialize 事件。可以使用 dominitialize 事件按照适当的页面构造顺序尽早建立沙箱桥,以便页面中定义的所有脚本均能访问该沙箱桥。

以下示例说明如何创建父级沙箱桥,以响应从子级 frame 调度的 dominitialize 事件:

<html> 
<head> 
<script> 
var bridgeInterface = {}; 
bridgeInterface.testProperty = "Bridge engaged"; 
function engageBridge(){ 
    document.getElementById("sandbox").contentWindow.parentSandboxBridge = bridgeInterface; 
} 
</script> 
</head> 
<body> 
<iframe id="sandbox" 
            src="http://www.example.com/air/child.html"  
            documentRoot="app:/" 
            sandboxRoot="http://www.example.com/air/" 
            ondominitialize="engageBridge()"/> 
</body> 
</html>

以下 child.html 文档说明子级内容如何访问父级沙箱桥:

<html> 
    <head> 
        <script> 
            document.write(window.parentSandboxBridge.testProperty); 
        </script>   
    </head>   
    <body></body> 
</html>

若要侦听子级窗口(而非框架)上的 dominitialize 事件,必须将侦听器添加到通过 window.open() 函数创建的新子级 window 对象:

var childWindow = window.open(); 
childWindow.addEventListener("dominitialize", engageBridge()); 
childWindow.document.location = "http://www.example.com/air/child.html";

在这种情况下,无法将应用程序内容映射到非应用程序沙箱。只有在从应用程序目录外部加载 child.html 时,此方法才有用。仍可将窗口中的应用程序内容映射到非应用程序沙箱,但必须首先加载一个中间页,在中间页中使用框架来加载子级文档并将其映射到所需的沙箱。

如果使用 HTMLLoader 类的 createRootWindow() 函数来创建窗口,则新窗口不是从中调用 createRootWindow() 的文档的子级。因此,无法从调用窗口建立到加载到新窗口中的非应用程序内容的沙箱桥。而是必须在新窗口中加载一个中间页,在中间页中使用框架来加载子级文档。这样,就可以在新窗口的父级文档与加载到框架中的子级文档之间建立沙箱桥。