Understanding the Security Model in Tauri v2

A deep look at Tauri’s core security architecture, trust boundaries, IPC guardrails, deny-by-default design, CSP, and the isolation pattern.

A Tauri desktop application is two separate processes that live in the same package. One is the Rust backend — the core — which runs with the full privileges of the user who launched the app. The other is the webview frontend — your React app — which is essentially a browser tab without an address bar. If these two sides could talk freely, a single line of compromised JavaScript in your frontend could read, write, or delete any file the user owns. Tauri’s security model exists to ensure that never happens by default.

The Two Worlds of a Tauri App

Every Tauri v2 application has a hard boundary between two trust zones.

  • The Core (Rust side): This process has full access to the operating system — file system, network, processes, environment variables. Plugins and your own tauri::command functions live here. Nothing restricts what Rust code can do except the OS’s own permission system.
  • The WebView (Frontend side): This is the rendered HTML, CSS, and JavaScript. By default, it can do almost nothing outside of its own sandbox. It cannot read files, spawn processes, or open network connections beyond what the browser engine allows — unless the core explicitly grants that ability.

The physical separation means that even if an attacker manages to execute arbitrary JavaScript inside the webview, they hit a locked door before touching the user’s files. That door is the IPC (Inter-Process Communication) layer, and it does not open by itself.

The IPC Bridge and Why It Is the Gatekeeper

When your React code calls a Tauri API — for example, reading a file — it does not call into the operating system directly. Instead, it sends a message over IPC to the Rust core.

[WebView] ----invoke("read_file", { path })----> [Rust Core]

The core receives the message, checks whether the caller is actually allowed to make this request, and then either executes the command or returns an error. Every single request crosses the bridge, and every single request gets checked.

IPC is not a direct API call:

Frontend code never links against system libraries. invoke is the only way to reach the core. This constraint is architectural, not opt-in — the webview process literally cannot call OS-level file or network functions.

This design flips the traditional desktop security problem on its head. Instead of asking “what should we block?”, Tauri asks “what tiny set of actions do we want to allow?”.

Deny-by-Default and the Principle of Least Privilege

A newly created Tauri v2 project grants zero custom permissions to the frontend. If you write a #[tauri::command] that deletes a directory and then try to invoke it from your React code, the invocation will fail with a permission error — even though the Rust function itself has no restrictions.

// Rust side — completely unrestricted
#[tauri::command]
fn delete_all_user_data() {
    std::fs::remove_dir_all("/important/data").unwrap();
}
// React frontend — this will be blocked
import { invoke } from "@tauri-apps/api/core";
async function tryDelete() {
  try {
    await invoke("delete_all_user_data");
  } catch (e) {
    console.error(e); // Permission denied
  }
}

A command existing in Rust is not enough:

Writing a Rust function and marking it with #[tauri::command] does not make it callable from the frontend. You must also explicitly declare that the command is allowed through the capability and permission system. Until then, the IPC layer rejects every invocation.

The model follows the Principle of Least Privilege: an application starts with no abilities beyond displaying a window, and the developer must explicitly list which specific actions each window is allowed to perform.

How Permissions, Scopes, and Capabilities Fit Together

Before looking at code that actually works, it helps to know the three pieces of the model without diving into their configuration files (those are covered in the Understanding Capabilities section). Think of them like this:

  1. Permission — a named declaration that says “command X can be called, and may be restricted to certain arguments.” For example, fs:allow-read-file with a scope that limits it to $HOME/documents/*.
  2. Scope — a filter attached to a permission that defines the allowed values for parameters. A file-read permission might have a scope allowing only .txt files under a specific folder.
  3. Capability — a bundle of one or more permissions assigned to one or more windows. A capability file says: “Window main gets these permissions.”

At runtime, when the frontend invokes a command, the core looks up the permission for that command, checks if the calling window’s capability includes it, and then validates the arguments against any scopes. If any check fails, the invocation is denied.

A working command demonstrates the model:

The following example shows a command that does work because it’s been explicitly allowed in the app’s capabilities. This is the pattern you aim for after understanding the security model.

Rust side — a command to read a file from an allowed directory:

// src-tauri/src/lib.rs
#[tauri::command]
fn read_notes_file() -> Result<String, String> {
    // This path is within an allowed scope configured in capabilities
    let content = std::fs::read_to_string("/home/user/Documents/notes.txt")
        .map_err(|e| e.to_string())?;
    Ok(content)
}
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_notes_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

React frontend — invoking the allowed command:

import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
function NotesReader() {
  const [content, setContent] = useState("");
  async function loadNotes() {
    const result = await invoke<string>("read_notes_file");
    setContent(result);
  }
  return (
    <div>
      <button onClick={loadNotes}>Load Notes</button>
      <pre>{content}</pre>
    </div>
  );
}
export default NotesReader;

The critical distinction is that the frontend never decides which files are accessible — the scope attached to the permission decides that on the core side. Even if someone modifies the React code to try a different path, the core will reject the call because the scope does not permit it.

Content Security Policy as a Second Layer

Even with IPC locked down, the webview itself can be a target. A cross-site scripting (XSS) attack could inject malicious JavaScript that attempts to abuse the Tauri APIs the window is allowed to use. Content Security Policy (CSP) restricts what the webview is allowed to load and execute.

Tauri v2 applies a default CSP that:

  • Disallows inline scripts (<script>alert(1)</script> will not run).
  • Blocks eval() and similar dynamic code execution.
  • Restricts network connections to only the IPC bridge (ipc: and http://ipc.localhost).

A typical CSP configuration in tauri.conf.json:

{
  "app": {
    "security": {
      "csp": {
        "default-src": "'self'",
        "connect-src": "ipc: http://ipc.localhost",
        "img-src": "'self' asset: http://asset.localhost blob: data:",
        "style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
      }
    }
  }
}

Disabling CSP removes a crucial barrier:

Adding 'unsafe-eval' or 'unsafe-inline' to script-src might unblock a library you need, but it also reopens the door to injected code. If your frontend dependencies require these directives, evaluate whether they can be replaced with CSP-compatible alternatives.

The Isolation Pattern — Guarding Against Untrusted Frontend Code

Sometimes the frontend itself cannot be fully trusted. You might load user-created plugins, include third-party scripts from a CDN, or embed external content in an iframe. In these cases, even the allowed Tauri APIs become dangerous — a malicious plugin could call the file-reading command you carefully granted, but with malicious intent.

Tauri v2 offers the Isolation Pattern as an optional extra shield. Instead of the frontend calling invoke() directly, all IPC messages pass through a separate, sandboxed JavaScript application that you control. This isolation app can inspect, modify, or reject any IPC request before it reaches the Rust core.

The flow becomes:

[Main Frontend] --> [Isolation App (iframe, sandboxed)] --> [Rust Core]

Because the isolation app runs in a separate sandboxed iframe with its own origin, even if the main frontend is compromised, the attacker cannot bypass the isolation layer. The isolation app can enforce additional rules — for example, only allowing read operations and never writes, regardless of what permissions the window technically has.

The isolation pattern is optional but powerful:

Most applications are safe with just capabilities and CSP. The isolation pattern becomes valuable when you load code you don’t control — plugins, user scripts, or remote content that needs limited API access.

Process Sandboxing at the Operating System Level

Beyond the architectural boundaries inside Tauri, the operating system itself provides additional sandboxing. On macOS, Tauri apps can adopt the App Sandbox entitlement, which restricts the entire application’s access to files, network, and hardware. On Windows, AppContainer isolation can be used. On Linux, seccomp filters can limit the system calls the process can make.

These OS-level protections are not configured by default in Tauri v2 projects, but they can be added for applications that need distribution through official app stores or handle extremely sensitive data. They provide defense in depth: even if the Rust core were somehow tricked into executing dangerous operations, the OS sandbox would still block them.

How This Compares to Other Desktop Frameworks

In many traditional desktop frameworks that embed a full Node.js runtime inside the renderer, any script running on a page can require('fs') and read arbitrary files — unless the developer explicitly disables Node integration. If a single dependency in node_modules ships malicious code, it gets the same file system access as the application itself.

Tauri’s model prevents this entirely by never giving the webview direct system access. The webview process literally cannot call file APIs; it can only send messages. And each message is validated against a permission list that the developer curates.

This architectural difference means the attack surface in a Tauri app is a fraction of what a runtime-sharing framework would have. There is no runtime to disable, because there was never any runtime exposed in the first place.

Common Misconceptions and Mistakes

Assuming all Rust commands are automatically available. Every command must be explicitly allowed through permissions. If you forget, the frontend will receive a permission denied error. This is the most frequent confusion for developers moving from other frameworks.

Using wildcard scopes as a shortcut. A scope like **/* grants access to every file the user can read. While convenient during development, it removes the safety net the scope system provides. A compromised frontend that can call a file-read command would then have unrestricted read access. Prefer narrow scopes that match your app’s actual needs.

Disabling CSP to make a library work without checking alternatives. Many libraries that require eval() have CSP-compatible versions or can be replaced. Turning off CSP is a permanent reduction in defense that affects every script in the webview, not just the one library.

Mixing OS-level and Tauri-level permission concepts. The OS may still block file access (e.g., macOS privacy protections) even when Tauri’s own permission system allows it. The error messages will look like PermissionDenied (os error 1), which is different from Tauri’s IPC-level permission rejection. The diagnostic path is: first check Tauri capabilities, then check OS file permissions and privacy settings.

Tauri cannot override OS-level restrictions:

On macOS, accessing certain directories (like Desktop, Documents, Downloads) requires the user to grant file access permission through the system prompt. Even if your capability file permits everything, the OS gate still stands.

Summary

The Tauri v2 security model rests on a single principle: the frontend cannot do anything dangerous unless you say so, and even then, only within the boundaries you draw. The core process runs with full privileges, but the webview process is physically separated and communicates only over a guarded IPC bridge. Commands are deny-by-default; permissions and scopes define what is allowed; capabilities assign those permissions to specific windows. CSP hardens the webview against injection, and the isolation pattern adds a programmable filter for untrusted frontend code. Together, these layers mean that a compromised frontend dependency does not automatically become a compromised user machine.

To make your application functional, you now need to define exactly which abilities it should have.