12k
All articles

Deno 权限模型详解

Deno 权限模型解析:allow 与 deny 标志、作用范围、deno.json 权限集、Permissions API,以及常见安全陷阱。

OpenReplay Team
OpenReplay Team
Deno 权限模型详解

Deno 在一个默认不授予任何权限的沙箱中运行你的代码:文件系统、网络、环境变量、子进程、系统信息以及原生库(FFI)全部处于关闭状态,直到你用 --allow-* 标志逐项开启为止;而且几乎每个标志都接受一个参数,用于把授权范围收窄到指定的路径、主机或变量。

如果你是从 Node 转过来的,你写的第一个 Deno 脚本几乎必然会因为权限错误而直接中断,而解决办法很少是肌肉记忆里那个一把梭的 -A。搞清楚该加哪个标志、以及范围该收到多紧,正是学习曲线的主要部分。

这与 Node.js 历来的默认行为正好相反,也是运行任何 Deno 脚本之前最需要理解的一件事。本文将逐一拆解:默认被阻止的能力有哪些、每一个 --allow-*--deny-* 标志及其范围语法、Deno 2.x 的新增内容(deny 优先、--allow-sys--allow-import、env 通配符)、deno.json 中的权限集、运行时 Permissions API,以及文档中提及最少的两个安全”暗坑”。

要点速览

  • 默认情况下,Deno 代码无法读写文件、建立网络连接、读取环境变量、派生子进程、访问系统信息或加载原生库。你需要通过 --allow-* 标志按能力逐项选择开启。
  • 每个 --allow-* 标志都有对应的 --deny-*,且 deny 始终优先:--allow-read=. --deny-read=./secrets 授权整个项目目录,但 ./secrets 仍然不可读。
  • 在 Deno 2 中,被拒绝的能力会抛出 Deno.errors.NotCapable(由旧的 PermissionDenied 重命名而来),从而把 Deno 的权限拒绝与普通的操作系统错误区分开来。
  • 自 Deno 2.5 起,你可以在 deno.json 中定义具名权限集,并用 -P=name 应用(或用裸 -P 应用 default 权限集),把最小权限标志纳入版本控制。
  • 初始静态 import 图中的任何内容在加载前都不会经过权限系统的检查,而 --allow-run 派生的子进程运行在沙箱之外。这就是不可信代码逃逸的两条路径。

为什么 Deno 默认是安全的?

没有任何东西带着环境权限(ambient privileges)运行:磁盘、网络、环境变量和子进程派生全部保持关闭,直到你主动打开。这一设计决策直接来自 Node 的最初作者 Ryan Dahl,他构建 Deno 就是为了扭转 Node “对一切都有完全访问权”的默认行为。在 Deno 中,依赖本身不会获得任何环境权限;而在 Node 中,一个包会继承所在进程能够触及的全部系统 I/O,这一差距是两个运行时之间最鲜明的区别。

Node 后来也加入了自己的权限模型。它在 Node 20 中以 --experimental-permission 的形式实验性发布,在 v23.5.0 中被标记为稳定,而 Node 24 则弃用了实验性写法,改用简洁的 --permission。Deno 的模型依然更为深入:它是默认行为而非可选标志,覆盖的能力类别更多,范围控制也更细。

Deno 的 —allow-* 权限标志有哪些?

每一类能力对应一个标志,且大多数标志接受一个允许列表参数。裸标志会授予该类别下的全部权限;带参数则会收窄范围。裸 --allow-net 授予对所有主机所有端口的访问权,而 --allow-net=api.example.com:443 则把程序限制为仅访问这一个主机和端口。

标志守护的能力限定范围示例对应的 deny 标志
--allow-read文件系统读取--allow-read=./data,config.ini--deny-read
--allow-write文件系统写入--allow-write=./tmp--deny-write
--allow-net网络访问--allow-net=api.example.com:443--deny-net
--allow-env环境变量--allow-env=PORT,HOST--deny-env
--allow-run子进程--allow-run=git,deno--deny-run
--allow-sys系统信息 API--allow-sys=hostname--deny-sys
--allow-ffi原生库--allow-ffi=./lib.so--deny-ffi
--allow-import远程 HTTPS 导入--allow-import=jsr.io--deny-import

注意,已不存在 --allow-hrtime。该标志在 Deno 2.0 中被移除,如今像 performance.now() 这样的高精度计时 API 始终可用。

当脚本需要一项你未授予的权限时,Deno 会暂停并交互式提示:

┏ ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

回答 y 表示本次授予,n 表示拒绝(会抛出 Deno.errors.NotCapable),A 表示允许整个类别。在 CI 环境中,请预先传入标志,以免因等待提示而阻塞。

标志集是如何演进的:deny 优先、--allow-sys--allow-import、env 通配符

deny 标志在 Deno 1.36(2023 年 8 月)中落地,此后每个 --allow-* 标志都配有对应的 --deny-*。两者重叠之处,生效的是拒绝规则,这让你可以先宽泛授权,再挖出例外:

deno run --allow-read=. --deny-read=./secrets app.ts

--allow-sys 可追溯至 Deno 1.26(2022 年 10 月),它管控诸如 Deno.hostname()Deno.systemMemoryInfo() 之类的系统信息 API。Deno 2.0 中唯一真正新增的能力类别是 --allow-import,它决定你的代码在运行时可以从哪些 HTTPS 主机拉取模块;纯 HTTP 从不被允许,静态 import 会自动按该列表过滤,而且指定你自己的主机是替换Deno 的内置集合,而非在其之上追加。使用 --deny-import 可以直接彻底屏蔽特定主机。

环境变量访问在 Deno 2.1 中获得了后缀通配符支持。无需逐个列出变量,可按前缀限定范围:

deno run --allow-env="AWS_*" main.ts

deno.json 中声明权限

Deno 2.5 起,你可以在 deno.json 中定义具名权限集,并用 -P=name(或 --permission-set=name)应用,从而把最小权限标志纳入版本控制,而不必每次运行都重新输入。对象的键就是标志名(readwritenetenvsysrunffiimport),详见 deno.json 参考文档

{
  "permissions": {
    "default": {
      "read": ["./deno.json"],
      "env": true,
      "run": { "allow": ["git"] }
    },
    "process-data": {
      "read": ["./data"],
      "write": ["./data"]
    }
  },
  "tasks": {
    "dev": "deno run -P main.ts"
  }
}

运行 deno run -P=process-data main.ts 使用具名权限集,或运行 deno run -P main.ts 使用 default 权限集。Deno 2.5 还新增了 DENO_AUDIT_PERMISSIONS 环境变量:将其指向一个文件路径,Deno 就会为程序触及的每一项权限追加一条 JSONL 记录,无论该访问是被授予还是被拒绝。这是快速摸清一个脚本真正需要什么权限的好办法。

运行时 Permissions API

在执行受限操作之前先在代码中查询权限,可以优雅降级,而不是直接因 NotCapable 错误崩溃。Deno.permissions 提供了 queryrequestrevoke,每个都接受一个形如 { name: "net", host: "example.com" } 的描述符:

const desc = { name: "net", host: "example.com" } as const;

let status = await Deno.permissions.query(desc); // "prompt" | "granted" | "denied"
if (status.state === "prompt") {
  status = await Deno.permissions.request(desc); // triggers the y/n/A prompt
}

if (status.state === "granted") {
  await fetch("https://example.com");
}

await Deno.permissions.revoke(desc); // drop it again

query 报告当前状态且不触发提示,request 在状态仍为 prompt 时向用户发起提示,revoke 则交还某项能力。这让长时间运行的程序可以在触及资源之前先做检查,并在访问不可用时走另一条路径。

两个暗坑:imports 与 --allow-run

有两种行为会让不可信代码绕过沙箱,两者都值得牢记于心。第一,凡是 Deno 能从你的入口点静态解析出来的东西(本地文件、npm 与 JSR 包,以及以字符串字面量写死的远程 URL)都会在权限系统介入之前被抓取,因此一个依赖可以在你的第一个 --allow-* 标志生效之前就读取自身源码并访问网络。这张”免票”仅限于加载阶段,别无其他:代码一旦执行,每个操作都会重新受到检查。--allow-import 限定的是哪些远程主机可被导入,但并不会让 import 本身需要运行时授权,所以在引入第三方代码之前请先审计。

第二,--allow-run 是最锋利的暗坑:你派生出来的东西会成为一个独立的进程,携带的是操作系统赋予它的权限,而不是你交给 Deno 的那一小套权限。也就是说,--allow-run=deno 会让一个受沙箱约束的脚本用 --allow-all 重新启动 Deno,从而完全逃逸。它还只限制哪个可执行文件能运行,而不限制其参数:--allow-run=cat 就意味着代码可以通过 cat 读取任意文件。请把它限定到特定的可信二进制文件,例如 --allow-run=git;同时请注意 --allow-ffi 存在同一类风险,因为原生库以机器码形式执行,处于 JavaScript 层检查之外。

实践立场是:授予能跑通的最窄允许列表,对敏感路径叠加 --deny-*,并把 --allow-run--allow-ffi 当作信任边界而非便利工具。从零权限起步,运行脚本,然后严格按照提示(或 DENO_AUDIT_PERMISSIONS 日志)告诉你的内容逐项补回。

常见问题

Deno 权限提示中选择拒绝与 Deno.errors.NotCapable 有什么区别?

两者是同一结果的不同入口。当你对交互式提示回答 'n',或者在缺少所需标志的情况下运行时,被拒绝的操作会在 Deno 2 中抛出 Deno.errors.NotCapable(由旧的 PermissionDenied 重命名而来)。这次重命名让你可以把 Deno 自身的权限拒绝与诸如文件缺失之类的普通操作系统错误区分开来,因为二者此前呈现出的失败形态非常相似。

--allow-net=example.com 是否也允许 443 端口上的 HTTPS?

是的。当你只指定主机而不指定端口时,例如 --allow-net=example.com,Deno 会允许连接该主机的任意端口,包括 443。若要限制为单个端口,必须显式写成 --allow-net=example.com:443,这样该主机上的其他所有端口都会被阻止。不带参数的裸 --allow-net 则授予所有主机所有端口的访问权。

我可以在重叠路径上同时使用 --allow-read 和 --deny-read 吗?

可以,而且 deny 始终优先。运行 --allow-read=. --deny-read=./secrets 会授予对整个项目目录的读取权,但 ./secrets 除外,它仍然不可读。在每一类能力中,deny 标志都优先于 allow 标志,因此这种模式让你可以宽泛授权后再挖出敏感路径,而不必逐个列举每一个允许的文件。

使用 npm 或 JSR 包需要 --allow-import 吗?

不需要,对于静态导入的包无需如此。凡是 Deno 无需执行代码即可从入口点解析出来的内容,包括 npm 和 JSR 包,都会在权限系统被征询之前被抓取。--allow-import 决定的是远程 import 可以来自哪些 HTTPS 主机,而纯 HTTP 从来都不是选项。运行时计算出的说明符则不同:动态远程 URL 需要 --allow-import,动态本地路径需要 --allow-read。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.