FORMA

权限模型

Deno 权限模型(核心安全特性)

Deno 默认在安全的沙盒环境中执行任何代码——无论是你的主程序还是第三方依赖,都没有文件、网络、环境变量或子进程的访问权限。只有通过 命令行标志显式授予 的权限才能被使用。

一、权限标志速查表

权限标志说明
--allow-read允许读取文件系统。可以限定路径:--allow-read=/etc,/tmp
--allow-write允许写入文件系统。同样可限定写入目录
--allow-net允许网络访问。可限定域名或 IP:--allow-net=api.example.com,localhost
--allow-env允许读取环境变量(默认无法访问 process.env
--allow-run允许运行子进程(现应使用 new Deno.Command()Deno.run() 已在 Deno 2 中软移除,仅保留实现、类型已删除)
--allow-ffi允许调用外部函数接口(Foreign Function Interface)
-A--allow-all授予所有权限(仅在开发 / 测试原型时使用,生产环境不推荐)

--deny-* 标志:在已 --allow-* 的前提下,可进一步拒绝特定路径或域名;deny 优先于 allow。例如:

bash
deno run --allow-net --deny-net=github.com,jsr.io script.ts

详见 Permissions

二、常用命令示例

bash
# 允许读取文件(无限制) + 网络访问
deno run --allow-read --allow-net server.ts

# 限制文件读取路径仅 /data 目录,网络仅允许访问 api.example.com
deno run --allow-read=/data --allow-net=api.example.com main.ts

# 授予所有权限(快速原型,非生产)
deno run -A main.ts

三、动态权限请求(运行时)

可以用 Deno.permissions.request() API 在代码中询问用户是否授予某个权限:

typescript
// 请求网络权限
const status = await Deno.permissions.request({ name: "net" });
if (status.state === "granted") {
  await fetch("https://deno.land");
} else {
  console.error("网络权限未授予");
}

其他动作包括 query(查询当前状态)、revoke(撤销权限)。这个机制适合交互式 CLI 工具,让用户按需授权。

四、安全模型的要点

  • 同等对待主程序和依赖:Deno 不区分你的代码和第三方包,两者共享同一个权限集。换句话说,如果一个依赖被授予了 --allow-net,它同样可以发起网络请求。
  • 最小权限原则:建议只添加程序确实需要的标志,并使用路径/域名限制,避免使用 -A
  • 无法动态提升权限Deno.permissions.request 只能降级或询问已在命令行声明范围的权限(例如 --allow-net 开启了网络权限但未限定域名,则请求时会弹出确认框;如果根本没有 --allow-net,则请求会被直接拒绝)。
  • 生产部署:通常直接将必要的权限标志写入 Dockerfile 或启动脚本中,不依赖动态弹窗。

五、精细化权限:路径与域名限定

粗粒度的 --allow-read/--allow-net 已经比 Node 默认全放开安全得多,但更推荐进一步限定范围:

bash
# 只允许读取 ./config 与 ./data 两个目录
deno run --allow-read=./config,./data main.ts

# 只允许访问指定的 API 域名,禁止访问其他任意网址
deno run --allow-net=api.example.com,cdn.example.com main.ts

# 同时使用 allow 与 deny:允许所有网络,但明确拒绝内网地址段
deno run --allow-net --deny-net=169.254.169.254 fetch-external.ts

最后一个例子在防止 SSRF(服务端请求伪造)攻击时很实用:即使代码存在漏洞被诱导访问任意 URL,也无法访问云厂商的内网元数据服务。

六、测试中的权限控制

deno test 同样遵循权限模型,测试文件默认无权限,需要什么权限就显式声明,这能帮助及早发现测试对外部资源的隐式依赖:

bash
deno test --allow-net=localhost:8080 --allow-env=NODE_ENV

也可以在测试代码中用 Deno.testpermissions 选项为单个测试单独声明所需权限,避免整份测试文件的权限范围被无意放大:

typescript
Deno.test({
  name: "读取配置文件",
  permissions: { read: ["./fixtures/config.json"] },
  fn() {
    // 仅在此测试内生效的只读权限
  },
});

七、与 Node.js 权限模型的对比

维度DenoNode.js
默认权限默认全部拒绝默认进程级全部允许
第三方依赖与主程序共享同一权限集,可被限制依赖天然拥有与主程序相同的能力,无原生隔离
授权方式命令行标志 / 运行时动态请求无内建机制,需依赖第三方沙箱库(如 vm2,且历史上出现过逃逸漏洞)
适合场景运行不完全信任的第三方代码、插件系统传统全信任的服务端应用

这也是为什么 Deno 常被用于运行用户提交的脚本(在线代码练习平台、Serverless 函数网关等)场景——权限模型天然提供了一层沙箱。

常见坑

  • 以为 --deny-net 能单独生效deny 只在对应的 allow 已经放开的前提下“做减法”,如果本身没有 --allow-net,网络请求依旧会被直接拒绝,不需要也不应该额外加 deny
  • 开发环境长期使用 -A 导致习惯性带入生产:本地为了方便加了 -A,上线脚本直接复制粘贴,权限收紧的收益就完全丧失了。
  • 以为权限是按“模块”而不是按“进程”授予:所有代码(主程序 + 全部依赖)共享同一份权限集合,无法只给某个第三方包单独限权(除非拆分为独立子进程)。
  • 忘记 --allow-env 也可以限定具体变量名--allow-env=PATH,HOME 比无限制的 --allow-env 更安全,很多人不知道该标志同样支持逗号分隔的白名单。

最佳实践

  1. 遵循最小权限原则:先不加任何权限运行,根据报错逐一补充需要的 --allow-*,而不是一开始就 -A
  2. 涉及网络、文件的标志尽量加上具体的域名/路径限定,而不是完全开放。
  3. 面向不受信任代码(插件、用户脚本)的场景,考虑结合 Deno.Command/子进程 + 更严格的权限组合,把风险隔离到独立进程。
  4. 在 CI 与生产 Dockerfile 中把最终确定的权限标志固化下来,作为该服务“需要哪些资源”的活文档。

延伸阅读

八、总结

Deno 的安全模型是其与 Node.js 最显著的差异之一。默认拒绝,显式授权的设计迫使开发者思考程序的真实需求,从而避免无意中引入安全漏洞。建议新人从 --allow-read--allow-net 等标志开始,逐步熟悉权限粒度控制。

参考文献

资料说明
Security官方权限说明
Permissions APIDeno.permissions
setup-denoCI 中的权限与安装

Series

deno

1 / 8

Deno 基础