Read OSS

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!"]
  1. 发现 capabilities —— 扫描 src-tauri/capabilities/ 目录,以及 tauri.conf.json 中内联定义的 capability 条目
  2. 加载插件 manifest —— 读取各插件在自身构建过程中生成的 ACL manifest
  3. 解析 permissions —— 展开 permission set 引用,合并 scope 值,并将每个命令映射到对应的访问规则
  4. 构建 BTree 查找结构 —— 最终产出是一个 Resolved 结构体,其中 allowed_commandsdenied_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() 会按以下步骤执行检查:

  1. 优先检查拒绝列表 —— 若命令出现在 denied_commands 中,且有条目匹配当前窗口/webview 标签和来源,则立即拒绝访问
  2. 检查允许列表 —— 在 allowed_commands 中查找命令,遍历已解析条目,通过 glob 匹配检查标签模式,并验证执行上下文(Local 对应应用内容,Remote { url } 对应外部 URL)
  3. 返回 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 自身是如何借助插件来实现其核心功能的。