开发者批评 Deno 权限系统安全性名不副实
来源标题:I'm not a fan of Deno's security model (2024)
一位开发者撰文认为 Deno 的权限系统存在根本性缺陷,宣称的「默认安全」具有误导性。他指出 Deno 的权限机制依赖标准库拦截对外访问,但无法真正隔离 JS 代码的越权行为。Deno 官网将权限系统列为旗舰特性,支持启动时指定 --allow-read、--allow-run 等权限,并可在运行时申请权限。
以下正文由 AI 翻译自 Hacker News(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
这个问题困扰我有一阵子了,所以我想把想法写下来,好从脑子里清出去,然后继续往前走。我们要聊的是 Deno 处理安全的方式,特别是它的权限系统,以及为什么我认为这种做法从根本上就不安全,而且有误导性。
Deno 是一个 JavaScript 语言运行时。它包含标准库、包管理器、TypeScript 转译器,还有一堆跟我们这个故事无关的东西。它不是 JavaScript 实现,所以它不解释也不编译 JavaScript,那部分它复用 V8。
Deno 的旗舰功能之一就是它的权限系统(在他们主页上挂在“Secure by default”标题下面)。思路很简单:因为跑在 Deno 里的 JS 代码需要标准库来访问外部世界,Deno 就可以选择允许或禁止哪些访问,所以当你运行
shelldeno run --allow-read=/tmp/dir --allow-run=uname code.ts
标准库就只会允许 code.ts 读 /tmp/dir 和运行 uname。
另外,程序可以在运行时请求权限,所以像这样的代码
typescriptconst r = { name: "read", path: "/tmp/dir" } as const; await Deno.permissions.request(r);
会在终端里显示一个挺好看的小提示:
┌ ⚠️ Deno requests read access to "/tmp/dir".
├ Requested by Deno.permissions.request() API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
然后你可以批准或拒绝它。
(顺便说一句:我真的很怀念浏览器扩展里有这个功能。现在,如果你的扩展需要潜在的访问任何网站的权限,它就得请求对所有网站的实际访问权限,这不太理想)
如果你喜欢解谜,这里就是你可以停下来想想这个系统会出什么问题的地方。
我想指出的两个具体问题里,这个是较轻的那个。我们先讨论它,因为这是我注意到的第一件事,而且我最初读到这个的时候,它确实立刻让我不爽。(如果你只对更劲爆的部分感兴趣,可以直接跳到下一节。)
那么,这个挺好看的小提示是显示在终端里的。它不是那种 PolKit 的 GUI 窗口之类的,它只是 stdout 上的文本,等着从 stdin 读输入。这做法太野了。
你看,终端的 API 有点怪。应用能发给终端的大多数命令/请求都是带内的,通过打印到 stdout 来发送。然后终端可以通过往 stdin 发数据来回复。由于 Deno 允许程序往 stdout 打印任意数据、从 stdin 读任意数据,而不需要任何权限也不做任何过滤,恶意代码就可以给终端发任意命令。这通常没问题,因为终端不会因为 stdout 让它执行 shell 命令它就执行,你本来应该能随便 cat 文件而不用冒任意代码执行的风险。但如果你允许恶意代码对终端为所欲为,然后又在同一个终端上打印提示,那就明显不太行了。
你可以,比如说,通过搞乱终端颜色来把提示藏起来。(这个在 2026 年被关掉了,因为权限提示是“部分可见”的(你能看到一个 emoji),而且“AI agent 倾向于”改终端颜色,所以那是个“有效用例”)或者骗用户预先往 stdin 里塞批准。或者骗终端把批准作为对某个请求的回复塞进 stdin。(终端永远不会往 stdin 发回车来避免执行命令,但“按回车继续”太常见了,不会让人起疑;用户根本看不到真正的提示。)
这里有个 asciicast,我觉得看起来挺酷的。
那个录屏里的具体问题早就修好了;终端颜色的问题没有。那其实不太重要,因为真正的问题是:把安全敏感信息放在攻击者控制的通道上。这就像在网页上执行攻击者控制的 JS 代码,然后在同一个页面上要密码。你没法知道页面处于安全状态,所以你没法信任任何输入。
即使 Deno 设法修好了“标准”转义码的每一个问题,你永远不知道外面还存在哪些非标准码。很多终端都有一些,你没法预测它们会怎么跟你的提示交互。
(顺便说一句:那 sudo 怎么就能做到呢?区别在于 sudo 是先问密码,在运行任何其他代码之前。要那样骗 sudo,你得在还没输入 sudo 之前就搞坏终端状态,而且得安静到你打字时注意不到。如果某个终端允许这样,我会认为那是终端的 bug)
那么,我上面写的那个命令有个怪东西(我复制到这里供参考):
shelldeno run --allow-read=/tmp/dir --allow-run=uname code.ts
--allow-run=uname 到底是什么意思?程序又不是有规范名字,不是 org.gnu.coreutils.uname,甚至不是 /nix/store/8k1dmzr3gwzqdgw45nzj76902ca7g1is-coreutils-full-9.5/bin/uname。它只是个快捷方式,意思是“逐个查看 PATH 里的所有目录,看能不能在那里找到 uname”。
你可以给子进程设置环境变量而不需要任何额外权限,那如果我们这样呢
$ cat t.ts const command = new Deno.Command("uname", { // my current working directory, can be anything env: { 'PATH': '/tmp/tmp.7ofmKaNW83' }, }); const { code, stdout, stderr } = command.outputSync(); console.log(new TextDecoder().decode(stdout)); $ cat uname #!/bin/sh printf lol $ deno run --allow-run=uname t.ts lol
是啊。
又一次,我们有了一个漂亮又方便的接口,但从安全角度看它根本说不通。如果程序能靠名字唯一标识就好了,但它们不能,而假装它们能就会导致安全漏洞。
好笑的是,我第一次折腾 Deno 的时候,甚至没想到会有这个问题,
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。