返回资讯中心
外部精选
软件工程
#开发者实时流

开发者批评 Deno 权限系统存在根本性安全缺陷

来源标题:I'm not a fan of Deno's security model (2024)

一位开发者撰文批评 Deno 的权限安全模型「从根本上不安全且具有误导性」。Deno 以「默认安全」为卖点,通过 --allow-read、--allow-run 等参数限制代码对文件系统和外部命令的访问,但作者认为这种基于标准库拦截的机制无法真正隔离恶意代码。文章还提到程序可在运行时通过 Deno.permissions.request 动态申请权限。

Hacker News 最新maxloh2026/11/8 · 今天27 分钟阅读

以下正文由 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 发送 Enter 来避免执行命令,但“按 Enter 继续”太常见了,不会让人起疑;用户根本看不到真正的提示)。这里有一个 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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →