Read OSS

Tauriのパーミッションシステム:Capabilities、ACL、そしてセキュリティ境界

上級

前提知識

  • 第1〜3回:アーキテクチャ、ライフサイクル、IPC
  • アクセス制御の基本概念(ACL、capabilities)の理解
  • Content Security Policy(CSP)の基礎知識
  • Webセキュリティの概念(XSS、プロトタイプ汚染)への慣れ

Tauriのパーミッションシステム:Capabilities、ACL、そしてセキュリティ境界

Tauriは内部にwebviewを抱えており、信頼できないコンテンツを実行される可能性があります。すべてのIPC呼び出しは潜在的な攻撃経路になり得ます。Tauriはこの課題に対し、コンパイル時とランタイムの両方で最小権限を強制する三層のパーミッションシステムで対応しています。この記事では、その仕組みを基礎から丁寧に説明していきます。

三層セキュリティモデル

Tauriのセキュリティモデルは、明確に区分された3つの層で構成されています。

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 構造体はプラットフォームの絞り込みもサポートしており、macOSやWindowsにのみ適用されるcapabilityを定義することも可能です。

Permissions は個々のコマンドへのアクセスを制御します。各permissionは、許可または拒否する一つ以上のIPCコマンドに対応します。プラグインは独自のpermissionを定義でき(例:fs:allow-read-file)、permission セットは複数のpermissionをひとつの名前にまとめます(例:fs:defaultには一般的な読み取り操作が含まれるなど)。

Scopes は、許可されたコマンドが何にアクセスできるかをさらに細かく制限します。たとえばfs:allow-read-fileというpermissionに対して、$HOME/Documents配下のファイルのみに絞り込むといったことが可能です。スコープはallow/denyの値リストとして定義され、プラグイン側がそれぞれのロジックに従って解釈します。

ACLモジュールのルートはcrates/tauri-utils/src/acl/mod.rsです。ここではAPP_ACL_KEY(アプリ定義のpermissionに使用)やACL_MANIFESTS_FILE_NAME(コンパイル済みマニフェストを格納するビルド成果物)といった重要な定数が定義されています。

Tip: 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. プラグインマニフェストの読み込み — 各プラグインが自身のビルド時に生成したACLマニフェストを読み込む
  3. permissionsの解決 — permissionセットの参照を展開し、スコープ値をマージして、各コマンドを解決済みのアクセスルールにマッピング
  4. BTreeルックアップ構造の構築 — 最終出力はResolved 構造体で、allowed_commandsdenied_commandsBTreeMap<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ラベルパターン、そして任意のスコープ参照が含まれます。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のラベルとoriginに一致すれば、即座にアクセスを拒否
  2. 許可コマンドの確認allowed_commandsでコマンドを検索し、解決済みエントリを順に確認。ラベルパターン(globマッチング)と実行コンテキスト(アプリコンテンツならLocal、外部URLならRemote { url })を検証
  3. Option<Vec<ResolvedCommand>>を返す — 許可された場合はスコープ情報を含むマッチした解決済みコマンドのSome、拒否された場合はNone

Origin enumはローカルのアプリコンテンツと外部URLを区別します。これにより、ローカルのwebviewにはファイル読み取りを許可しつつ、リモートコンテンツは安全なコマンドのホワイトリストのみに制限するといった細かな制御が可能になります。

dynamic-aclフィーチャーが有効な場合は、Manager::add_capability()を通じてランタイムにcapabilityを追加することもでき、影響を受けるコマンドのパーミッションはその場で再解決されます。

スコープ:きめ細かいアクセス制御

スコープはpermissionに構造化データを付加し、「このコマンドを許可/拒否する」という枠を超えた制約を実現します。ResolvedScopeはallowとdenyの値リストを持ちます。

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

スコープ値の解釈はプラグイン側の責務です。たとえばファイルシステムプラグインは、要求されたパスがallowパターンに一致し、かつdenyパターンに一致しないかを確認します。スコープ値はserde_json::Value型なので、プラグインごとに独自のスキーマを定義できます — FSプラグインならパス、HTTPプラグインならURLパターン、という具合です。

スコープは複数のcapability間でマージされます。二つのcapabilityがそれぞれ異なるパススコープでfs:allow-read-fileを付与している場合、有効なスコープは両方のallowリストの和集合になります。denyリストも同様にマージされ、denyは常にallowより優先されます。

Isolationパターンとインボークキー

ACLに加えて、Tauriはさらに二つのセキュリティ機構を提供しています。

インボークキーBuilder::new()で生成されるランダムなトークンで、初期化スクリプトを通じてすべてのwebviewに注入されます。第3回でも触れたように、すべてのIPCリクエストはこのキーをTauri-Invoke-Keyヘッダーに含めて送信し、on_message()ハンドラーは不正なキーを持つリクエストを無言で破棄します。CSPをすり抜けたスクリプトであっても、このキーなしにコマンドを呼び出すことはできません。

Isolationパターンはさらに一段階のサンドボックス化を提供します。有効にすると、Tauriはisolation://カスタムプロトコルから読み込まれた隠し<iframe>を生成します。メインのwebviewはこのiframeへpostMessageで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管理とプロトタイプ汚染対策

Tauriはtauri://プロトコルを通じて配信されるすべてのコンテンツに対して、Content Security Policyヘッダーを管理します。CSPヘッダーはtauri.conf.jsonで設定され、HTMLレスポンスに注入されます。開発時にはdevサーバーへの接続を許可するため、TauriがCSPを変更する場合もあります。

スクリプトの完全性を保つために、Tauriはインライン<script>タグにnonceを生成してCSPのscript-srcディレクティブに追加します。これにより、Tauri自身の初期化スクリプトは実行を許可しつつ、インジェクション攻撃はブロックされます。

最も意外なセキュリティ対策がscripts/freeze_prototype.jsです — 他のどのJavaScriptよりも先に実行される、たった一行のコードです。

Object.freeze(Object.prototype)

これはプロトタイプ汚染攻撃を防ぐための措置です。攻撃者がObject.prototypeを改ざんしてすべてのオブジェクトにプロパティを注入すると、IPCコールバックの横取りや__TAURI_INTERNALS__ブリッジの改ざんが可能になってしまいます。信頼できないコードが実行される前にプロトタイプをフリーズすることで、IPC基盤の健全性が保証されます。

Tip: リモートコンテンツを読み込むTauriアプリを開発する場合は、isolationパターンを有効にし、capabilitiesをできる限り絞り込んで定義しましょう。インボークキー検証、ACL強制、isolationサンドボックス、CSP、プロトタイプフリーズという多重の防御を組み合わせることで、攻撃者はそのすべてを突破しない限りRustバックエンドに到達できません。

次回は、Tauriのプラグインシステムがこのセキュリティ基盤の上にどのように構築されているかを探ります。そしてTauri自身がプラグインを活用してコア機能を実装している方法についても見ていきましょう。