为什么需要权限系统
想象你执行了 npm install some-package,然后 node app.js。此时这个 some-package 可以:
- 读取
~/.ssh/id_rsa(你的私钥) - 读取
~/.aws/credentials(AWS 凭证) - 向任意服务器上传这些文件
- 执行系统命令、安装后门
而你完全不知情。这在 npm 生态中已经发生过多次供应链攻击事件。
Deno 的权限系统从根本上解决了这个问题:默认沙盒,一切皆需授权。
权限标志完整参考
| 权限标志 | 控制范围 | 细化示例 |
|---|---|---|
--allow-read | 文件系统读取 | --allow-read=/tmp,./config |
--allow-write | 文件系统写入 | --allow-write=/tmp |
--allow-net | 网络访问(TCP/UDP) | --allow-net=api.example.com:443 |
--allow-env | 环境变量读写 | --allow-env=PORT,DATABASE_URL |
--allow-run | 执行子进程 | --allow-run=git,npm |
--allow-ffi | 调用动态库(FFI) | --allow-ffi=./libcustom.so |
--allow-hrtime | 高精度计时(防时序攻击) | 无法细化 |
--allow-sys | 系统信息(os/cpus/memory) | --allow-sys=hostname |
--allow-import | 从 URL 导入模块(默认允许标准域) | --allow-import=deno.land |
| --allow-all | 开放所有权限(等同 -A) | 生产环境慎用 |
细粒度权限控制
--allow-read:限制可读路径
# 仅允许读取 /tmp 和 ./config 目录
deno run --allow-read=/tmp,./config app.ts
# 尝试读取其他路径会报错:
# PermissionDenied: Requires read access to "/etc/passwd",
# run again with the --allow-read flag
--allow-net:限制可访问的主机
# 仅允许访问指定域名和端口
deno run --allow-net=api.stripe.com:443,db.internal:5432 app.ts
# 允许所有网络(不推荐)
deno run --allow-net app.ts
# Deno 2 支持通配符:
deno run --allow-net=*.internal.com app.ts
--allow-env:限制可读取的环境变量
# 仅允许读取指定环境变量
deno run --allow-env=PORT,DATABASE_URL,JWT_SECRET app.ts
// 代码中读取
const port = Deno.env.get("PORT") ?? "8000";
// 尝试读取未授权的变量会返回 undefined(非报错)
--allow-all 的生产风险
慎用 --allow-all(-A):deno run -A app.ts 等同于关闭了所有安全保护,与运行 Node.js 程序无异。在生产环境,你应该始终使用最小权限原则——只授予程序实际需要的权限。--allow-all 只适合开发调试时快速验证。
# 开发环境(方便调试,允许一切)
deno run -A --watch main.ts
# 生产环境(精确控制权限)
deno run \
--allow-net=0.0.0.0:8000,db.prod:5432 \
--allow-env=DATABASE_URL,JWT_SECRET,PORT \
--allow-read=/app/static \
main.ts
交互式权限提示
当你不加任何权限标志运行代码,Deno 会在程序尝试访问受限资源时弹出交互式提示:
$ deno run hello.ts
┌ ⚠️ Deno requests net access to "api.github.com".
├ Requested by `fetch()` on line 3.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net) >
选项说明:
y:本次允许(此次运行有效)n:拒绝(抛出 PermissionDenied 错误)A:允许所有网络访问(等同本次--allow-net)
deno.json 中配置权限
Deno 2 支持在 deno.json 的 tasks 中为不同任务配置不同权限,也可以通过 permissions 字段(deno 2.1+ 支持)设置默认权限:
// deno.json
{
"tasks": {
"start": "deno run --allow-net=0.0.0.0:8000 --allow-env=PORT,DB_URL main.ts",
"dev": "deno run -A --watch main.ts",
"test": "deno test --allow-read=./fixtures --allow-net=localhost"
}
}
运行时权限查询
程序可以在运行时检查自己拥有哪些权限:
// 查询权限状态
const netPerm = await Deno.permissions.query({ name: "net", host: "api.example.com" });
console.log(netPerm.state); // "granted" | "denied" | "prompt"
// 主动申请权限(会弹出交互提示)
const result = await Deno.permissions.request({ name: "read", path: "/tmp" });
if (result.state === "granted") {
// 用户已授权
}
// 主动撤销权限
await Deno.permissions.revoke({ name: "read", path: "/tmp" });
实战:最小权限 HTTP 服务器
下面是一个仅开放必要权限的生产级 HTTP 服务器示例:
// server.ts — 最小权限设计
// 启动命令:
// deno run --allow-net=0.0.0.0:8000 --allow-env=PORT --allow-read=./static server.ts
const PORT = Number(Deno.env.get("PORT")) || 8000;
Deno.serve({ port: PORT }, async (req: Request): Promise<Response> => {
const url = new URL(req.url);
// 静态文件服务(仅允许 ./static 目录)
if (url.pathname.startsWith("/static/")) {
const filePath = `./static${url.pathname.slice(7)}`;
try {
const file = await Deno.readFile(filePath);
return new Response(file);
} catch {
return new Response("Not Found", { status: 404 });
}
}
// API 端点
if (url.pathname === "/api/health") {
return Response.json({ status: "ok", time: new Date().toISOString() });
}
return new Response("Hello from Deno!");
});
console.log(`Server running on http://localhost:${PORT}`);
本章小结:Deno 的权限系统是其最核心的安全特性。所有权限默认关闭,程序只能在明确授权的范围内操作。通过细化权限(--allow-net=hostname、--allow-read=/path),可以实现最小权限原则,大幅降低供应链攻击风险。在 deno.json 的 tasks 中预设权限,是生产环境的最佳实践。