Tauri 的权限系统:Capabilities、ACL 与安全边界
前置知识
- ›第 1-3 篇:架构、生命周期与 IPC
- ›了解访问控制的基本概念(ACL、capabilities)
- ›Content Security Policy(CSP)基础知识
- ›熟悉 Web 安全概念(XSS、prototype pollution)
Tauri 的权限系统:Capabilities、ACL 与安全边界
Tauri 内嵌了一个 webview——一个可能执行不可信内容的环境。这意味着每一次 IPC 调用都是潜在的攻击面。Tauri 的应对方案是一套三层权限系统,在编译期和运行时同时强制执行最小权限原则。本文将从底层原理出发,逐一拆解这套机制。
三层安全模型
Tauri 的安全模型由三个独立层次构成:
flowchart TB
subgraph "Tier 1: Capabilities"
CAP["Capability<br/>'main-user-files'<br/>windows: ['main']"]
end
subgraph "Tier 2: Permissions"
PERM1["fs:allow-read-file"]
PERM2["dialog:open"]
PERM3["fs:allow-write-text-file"]
end
subgraph "Tier 3: Scopes"
SCOPE["allow: [{path: '$HOME/docs'}]<br/>deny: [{path: '$HOME/docs/.secret'}]"]
end
CAP --> PERM1
CAP --> PERM2
CAP --> PERM3
PERM3 --> SCOPE
Capabilities 负责将权限分配给特定的窗口或 webview。每个 capability 包含一个标识符、一组窗口/webview 标签匹配模式,以及一组权限列表。只有匹配到模式的窗口才会获得对应的权限。Capability 结构体还支持平台过滤,可以让某些 capability 仅在 macOS 或 Windows 上生效。
Permissions 用于管控具体的命令。每个 permission 映射到一个或多个允许或拒绝的 IPC 命令。插件可以定义自己的 permissions(如 fs:allow-read-file),permission sets 则将多个 permissions 归并为一个名称(例如 fs:default 可能包含常用的读操作)。
Scopes 在已授权命令的基础上,进一步约束可访问的资源范围。例如,fs:allow-read-file 权限可以被限定为只能访问 $HOME/Documents 下的文件。Scopes 以允许/拒绝值列表的形式定义,由各插件按照自身逻辑进行解释。
ACL 模块的根文件位于 crates/tauri-utils/src/acl/mod.rs,定义了一些重要常量,如 APP_ACL_KEY(用于应用定义的权限)和 ACL_MANIFESTS_FILE_NAME(存放编译后 manifest 的构建产物)。
提示: 可以这样理解这三层——capabilities 解决"谁能访问",permissions 解决"能访问哪些命令",scopes 解决"限定在哪些资源范围内"。没有匹配到任何 capability 的窗口,IPC 访问权限为零——连核心命令都无法调用。
编译期解析:从 Capabilities 到已解析命令
安全模型的主要解析工作在编译期完成。在 build.rs 阶段,tauri-build 的 ACL 模块会执行一套多步解析流程:
flowchart LR
CAP_FILES["Capability files<br/>(JSON/TOML)"] --> PARSE["Parse capabilities"]
MANIFESTS["Plugin ACL manifests"] --> MERGE["Merge permissions"]
PARSE --> MERGE
MERGE --> RESOLVE["Resolved::resolve()"]
RESOLVE --> BTREE["BTreeMap<String, Vec<ResolvedCommand>>"]
BTREE --> EMBED["Embedded in binary<br/>via generate_context!"]
- 发现 capabilities —— 扫描
src-tauri/capabilities/目录,以及tauri.conf.json中内联定义的 capability 条目 - 加载插件 manifest —— 读取各插件在自身构建过程中生成的 ACL manifest
- 解析 permissions —— 展开 permission set 引用,合并 scope 值,并将每个命令映射到对应的访问规则
- 构建 BTree 查找结构 —— 最终产出是一个
Resolved结构体,其中allowed_commands和denied_commands均为BTreeMap<String, Vec<ResolvedCommand>>:
pub struct Resolved {
pub has_app_acl: bool,
pub allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
pub denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
pub command_scope: BTreeMap<ScopeKey, ResolvedScope>,
pub global_scope: BTreeMap<String, ResolvedScope>,
}
每个 ResolvedCommand 携带了执行上下文(本地或远程)、以 glob 模式表示的窗口/webview 标签匹配规则,以及可选的 scope 引用。BTree 结构在运行时提供 O(log n) 的命令查找效率,完全满足 IPC 热路径的性能要求。
运行时执行:RuntimeAuthority
RuntimeAuthority 是运行时的安全守门人。它由编译期生成的 Resolved 结构体构建而来,以 Mutex 保护的形式存放在 AppManager 内部:
pub struct RuntimeAuthority {
has_app_acl: bool,
allowed_commands: BTreeMap<String, Vec<ResolvedCommand>>,
denied_commands: BTreeMap<String, Vec<ResolvedCommand>>,
pub(crate) scope_manager: ScopeManager,
}
每次 IPC 调用时,resolve_access() 会按以下步骤执行检查:
- 优先检查拒绝列表 —— 若命令出现在
denied_commands中,且有条目匹配当前窗口/webview 标签和来源,则立即拒绝访问 - 检查允许列表 —— 在
allowed_commands中查找命令,遍历已解析条目,通过 glob 匹配检查标签模式,并验证执行上下文(Local对应应用内容,Remote { url }对应外部 URL) - 返回
Option<Vec<ResolvedCommand>>—— 匹配成功则返回Some(含 scope 信息),否则返回None
Origin 枚举用于区分本地应用内容和远程 URL,实现精细化控制——例如允许本地 webview 读取文件,同时将远程内容限制为只能调用白名单中的安全命令。
当启用 dynamic-acl feature 时,还可以通过 Manager::add_capability() 在运行时动态添加 capabilities,系统会即时重新解析受影响命令的权限。
Scopes:细粒度访问控制
Scopes 为权限附加了结构化数据,支持在"允许或拒绝某个命令"之外进行更精细的约束。ResolvedScope 携带允许和拒绝两组值列表:
pub struct ResolvedScope {
pub allow: Vec<Value>,
pub deny: Vec<Value>,
}
classDiagram
class ResolvedCommand {
+context: ExecutionContext
+windows: Vec~Pattern~
+webviews: Vec~Pattern~
+scope_id: Option~ScopeKey~
}
class ResolvedScope {
+allow: Vec~Value~
+deny: Vec~Value~
}
class ScopeManager {
+command_scope: BTreeMap
+global_scope: BTreeMap
+get_command_scope(resolved) ScopeValue
}
ResolvedCommand --> ScopeManager : scope_id references
ScopeManager --> ResolvedScope : contains
插件负责解释 scope 值的具体含义。例如,文件系统插件会检查请求的路径是否匹配允许模式,且不在任何拒绝模式中。scope 值的类型是 serde_json::Value,因此每个插件可以定义自己的 schema——文件系统插件使用路径,HTTP 插件使用 URL 模式,以此类推。
Scopes 会跨 capabilities 合并:如果两个 capabilities 都授予了 fs:allow-read-file 但指定了不同的路径 scope,则最终生效的 scope 是两个允许列表的并集。拒绝列表同样会合并,且拒绝规则的优先级始终高于允许规则。
Isolation 模式与 Invoke Key
除 ACL 之外,Tauri 还提供了两种额外的安全机制:
Invoke Key 是在 Builder::new() 中生成的随机令牌,通过初始化脚本注入到每个 webview 中。正如第 3 篇所述,每次 IPC 请求都会在 Tauri-Invoke-Key 请求头中携带这个令牌,on_message() 处理器会静默丢弃令牌不正确的请求。这可以防止绕过 CSP 的脚本发起未经授权的 IPC 调用——没有令牌,命令根本无法触达。
Isolation 模式在此基础上增加了一层沙箱隔离。启用后,Tauri 会创建一个隐藏的 <iframe>,通过 isolation:// 自定义协议加载。主 webview 通过 postMessage 向该 iframe 发送 IPC 消息,iframe 中的 JavaScript(来自 scripts/ipc.js)对消息进行验证和加密后,再转发给 Rust 后端。这意味着即便攻击者在主 webview 中实现了 XSS,也无法直接调用 IPC——他们还需要突破 isolation iframe,而那里运行的是你自己控制的代码。
flowchart LR
MAIN["Main Webview"] -->|"postMessage"| IFRAME["Isolation iframe<br/>(isolation:// protocol)"]
IFRAME -->|"validated + encrypted"| IPC["ipc:// protocol"]
IPC --> RUST["Rust Backend"]
ATTACKER["XSS Attacker"] -.->|"❌ Can't bypass"| IFRAME
CSP 管理与 Prototype Pollution 防护
Tauri 会统一管理通过 tauri:// 协议提供内容的 Content Security Policy 响应头。CSP 在 tauri.conf.json 中配置,并在 HTML 响应中自动注入。开发模式下,Tauri 可能会修改 CSP 以允许连接到开发服务器。
在脚本完整性方面,Tauri 会为内联 <script> 标签生成 nonce,并将其添加到 CSP 的 script-src 指令中,从而让 Tauri 自身的初始化脚本正常执行,同时阻断注入攻击。
最出人意料的安全措施来自 scripts/freeze_prototype.js——一行在所有其他 JavaScript 之前运行的代码:
Object.freeze(Object.prototype)
这行代码可以防御 prototype pollution 攻击——攻击者通过修改 Object.prototype 向所有对象注入属性,从而可能劫持 IPC 回调或篡改 __TAURI_INTERNALS__ bridge。在任何不可信代码执行之前冻结原型,Tauri 确保了 IPC 基础设施的完整性不受破坏。
提示: 如果你的 Tauri 应用需要加载远程内容,请务必启用 isolation 模式,并定义尽可能窄的 capabilities。invoke key 验证、ACL 执行、isolation 沙箱、CSP 以及 prototype 冻结共同构成了多层纵深防御——攻击者需要逐一突破所有防线,才能最终触达你的 Rust 后端。
下一篇文章将探讨 Tauri 的插件系统如何构建在这套安全基础设施之上——以及 Tauri 自身是如何借助插件来实现其核心功能的。