Caputchin
理解 Caputchin

Caputchin 如何沙箱化游戏

一个 Caputchin 游戏是跑在你访客浏览器里、在你页面上的第三方代码。无论它来自应用市场还是你自己托管它,Caputchin 都把它当作 默认不被信任 的,并把它裹进几道独立的隔离层里,于是一个有 bug 的或敌意的游戏能渲染、能玩、能报告一个结果,却够不到别的任何东西:够不到你的页面、够不到你访客的数据、够不到网络。

这一页解释每一道措施、每一道为什么存在、以及这整件事如何和你加到自己站点上的一个 CSP 互动。

一行话讲清威胁模型

游戏跑由一个第三方提供的任意 JavaScript(以及可能的 WebAssembly)。因此隔离目标是:游戏能计算和绘制,能把一个结果交回给组件,但它无法读取或影响它自己那个框之外的任何东西。 下面每一层都服务于那一个目标,而且它们是纵深防御,没有单独一层被信任为完美。

第 1 层:一个不透明来源的沙箱化 iframe

每个游戏都跑在组件用一个刻意极简的 sandbox 属性构建的 <iframe> 里。被授予的 唯一 令牌是 allow-scripts,因为游戏是 JavaScript、必须跑。其他一切都被扣下,而最重要的省略是 allow-same-origin

没有 allow-same-origin,浏览器就给这个框一个独一的 null 来源。那一个事实就是那道承重的边界:

  • 游戏读不到你页面的 DOM、cookie、localStorage 或 Caputchin 会话,它被密封进一个一次性的、对你而言陌生的来源里。
  • 因为这个框也永远无法导航你的顶层窗口、打开弹窗、提交表单或弹出原生对话框(所有那些能力都是 Caputchin 不授予的沙箱令牌),所以没有一条从游戏回到你页面的路径。

allow-scripts 而无 allow-same-origin 的组合就是全部要点:浏览器执行游戏的代码,但把这个框当作一个碰不到你任何东西的陌生来源。Caputchin 从不给一个游戏框加 allow-same-origin。(作为一个后果,游戏发给组件的每条消息都来自来源 "null",组件的通道检查正预期它,一个非 null 的来源本身就会示意一个配置错误的沙箱。)

第 2 层:框的文档是内联构建的,不是拉取的

组件不把 iframe 指向一个远程页面。它 内联地(通过 srcdoc)构造框的整个 HTML 文档,并把它交给那个 null 来源的框。那个文档很小:一个严格的 CSP meta 标签(下一层)、一小段 Caputchin 运行时引导、以及一个加载游戏包的 <script> 标签。

这对两条交付路径是同一个机制,只有包的 URL 不同:

游戏来源内联文档里的包 URL
应用市场(平台解析)Caputchin 在挂载时解析出的那个固定的、带完整性哈希的包 URL。
自托管(你的 game-src你提供的那个 URL。

因为这个文档是由组件、而非游戏撰写的,游戏从不控制 CSP、运行时或框的结构,只控制它自己的包一经加载后做什么。

第 3 层:给应用市场游戏的子资源完整性

当 Caputchin 提供一个应用市场游戏时,它把包钉到一个不可变的版本,并记录它的一个加密哈希(SHA-384)。加载这个包的 <script> 标签带着那个哈希作为一个 integrity 属性、配 crossorigin="anonymous",于是如果有一个字节和被钉住的不同,浏览器自己 就拒绝执行这个包。一个被攻陷的 CDN 无法替换成不同的代码;加载失败关闭。

一个自托管的游戏没有平台断言的哈希(对你自己的字节,你是信任根),所以那条路径就干脆省略 integrity 属性。一个应用市场游戏的 结果 如何在服务器上被独立地重新推导,见 回放契约,那是一个和完整性分开的保证。

第 4 层:一个严格的内联 Content-Security-Policy

那个内联文档带着一个收紧的 Content-Security-Policy。即便这个框已经是 null 来源且沙箱化的,CSP 还进一步约束被加载的代码可以做什么,它默认拒绝一切,并只重新授予最低限度:

指令为什么
default-src'none'拒绝下面没有被明确允许的一切。
script-srcCaputchin 自己内联运行时的一个哈希 + 游戏包的来源 + 'wasm-unsafe-eval'只跑 Caputchin 那个确切的引导(由一个 sha256- 哈希钉住,而非 'unsafe-inline')和游戏自己的包。'wasm-unsafe-eval' 让一个 WASM 引擎编译,而不 授予完整的 'unsafe-eval'
connect-src'none'那条反外泄指令。 游戏无法 fetchXHR、打开一个 WebSocket 或向任何地方发信标。完全没有网络出口。
img-srcdata:(外加任何皮肤资源来源,见下文)精灵图内联,或来自一个被允许的资源主机。
media-srcdata:(外加任何皮肤资源来源,见下文)音频/视频内联,或来自一个被允许的资源主机。
font-srcdata:只内联字体。
style-src'unsafe-inline'游戏设置内联样式;没有外部样式表。(样式无法像脚本或网络那样外泄数据。)

净效果:游戏跑、渲染、并通过 SDK 桥报告它的结果,而够不到别的任何东西,尤其够不到网络。这个 CSP 由 Caputchin 设置,不是你配置的东西;它列在这里,是为了让你确切理解一个游戏被收容得有多紧。

一个客户设置加宽框 CSP 的唯一地方

皮肤可以把图像或音频字段指向你自己 CDN 上的一个绝对 URL(见 皮肤)。为了让那些资源能在这个锁死的框里加载,Caputchin 把 恰好那些资源来源 加到框的 img-src / media-src 上,别无他处。script-srcconnect-src 从不被加宽,于是即便一个被允许的资源来源也无法被用来加载代码或打开一条网络通道。这是一个被配置的值影响框策略的那唯一、狭窄的方式,而且它仍然只限资源。

第 5 层:一条单向的结果通道

游戏没有一个能调进你页面的面。出去的唯一方式是 SDK 桥:游戏调用单一一个方法,把它不透明的结果(它的轨迹)postMessage 给组件,而组件住在你的来源上。和 Caputchin 的 API 对话的是组件;游戏从不(它不能,connect-src'none')。所以那条信任路径是 游戏 → 组件 → 你的后端,而每一跳都只把一个结果往前传,绝不授予游戏向后的触及范围。

你设在自己页面上的那个 CSP

上面那些层保护 你和你的访客免受游戏之害。你自己页面上的一个 Content-Security-Policy 是一个不同的、互补的控制:它保护 你的页面免受一切之害,而 Caputchin 被设计为在一个严格的 CSP 下运行。你不必为了嵌入 Caputchin 而放松你的 CSP;你只需 允许组件正当使用的那少数几个来源。

至少,组件需要:

指令允许为什么
script-src你加载组件脚本的那个来源,外加 'unsafe-eval'那个定义 <caputchin-widget> / <caputchin-game><script>、你选的那个 CDN(jsDelivr、你自己的主机或 caputchin.com)。'unsafe-eval'探测 挑战能跑;探测开着时它是必需的,你可以通过在密钥的 安全 页上把探测关掉来去掉它。
connect-srchttps://caputchin.com组件调用 Caputchin API 来搭建并确认一次验证。如果你把组件指向一个不同的 API 主机,就改为允许那个。
frame-srchttps://caputchin.com管辖游戏 iframe。因为这个框用一个内联的 srcdoc 文档,浏览器把你的 frame-src 应用到它上,所以在这里允许 Caputchin 的来源。
worker-srcblob:proof-of-work 求解器完全跑在从一个 blob: URL 创建的 Web Worker 里,没有主线程回退,所以这是必需的。如果你省略 worker-src,浏览器会回退到 child-src 再到 default-src,而单凭 default-src 'self' 不允许 blob:

可选地把 'wasm-unsafe-eval' 加到 script-src:它让 proof-of-work 求解器作为快速的 WebAssembly 跑。它不是必需的,没有它求解器会退回到一个较慢的纯 JavaScript 实现(仍在 Worker 里)。

需要在你的页面 CSP 里允许游戏自己的包来源(jsDelivr、一个游戏的 CDN、你的 game-src 主机):那个包加载在 沙箱化的框里面、在上面描述的框自己的 CSP 之下,而不在你页面的策略之下。你的页面只框住 Caputchin;Caputchin 收容里面跑什么。

如果一个 Caputchin 元素在你的 CSP 下默不作声地加载失败,你的浏览器控制台会点名被挡的那条指令;把它点名的来源或关键词加到那条指令上。从严格起步,并恰好打开控制台所要求的。你永远不需要 'unsafe-inline'。你确实需要给 worker 的 blob:worker-src),而当探测开着时,需要 script-src 里的 'unsafe-eval';如果你在密钥的 安全 页上把探测关掉,那个 'unsafe-eval' 要求就消失。'wasm-unsafe-eval' 是可选的(它加速 proof-of-work 求解器,否则求解器在同一个 Worker 里以 JavaScript 跑得更慢)。

为什么这么多层

每一层单独都会是一道有意义的边界;合在一起它们意味着其中一层的失效不是一次失守。单凭 null 来源就已经把游戏和你的数据隔开;单凭 CSP 就已经掐死网络出口;单凭完整性就已经钉住那确切的字节。Caputchin 把它们全跑起来,因为不被信任的第三方代码恰恰是纵深防御物有所值的地方。

另见

本页内容