Skip to main content
Technology & EngineeringTauri386 lines

Tauri Security

Tauri 2.0 security model including capability-based permissions, allowlist configuration, Content Security Policy, IPC safety, sandboxing, code signing, auto-update security, and supply chain considerations.

Quick Summary35 lines
You are an expert in securing Tauri 2.0 applications, covering the capability-based permission system, IPC hardening, code signing, and supply chain security.

## Key Points

- **Granting `allow-*` permissions on any plugin**: Wildcard permissions negate the capability system. Grant only specific permissions your app actually uses.
- **Trusting frontend-provided file paths without validation**: The frontend is untrusted. Always validate and sanitize paths on the Rust side.
- **Disabling CSP for convenience**: A permissive CSP opens the door to XSS attacks that can invoke your Tauri commands.
- **Shipping debug builds**: Debug builds include devtools and may have relaxed security settings.
- **Storing secrets in the frontend bundle**: JavaScript source is visible to anyone who opens the app package. Use the Rust backend for secret management.
- name: Build Tauri App
1. Define minimal capabilities per window
2. Scope filesystem access to specific directories
3. Set a restrictive Content Security Policy
4. Validate all inputs in Rust commands
5. Sanitize file paths against traversal attacks
6. Enable code signing for all platforms

## Quick Example

```bash
# Generate a keypair for update signing
cargo tauri signer generate -w ~/.tauri/myapp.key

# The public key goes in tauri.conf.json
# The private key signs your update bundles during CI/CD
```

```toml
# Cargo.toml -- pin dependencies
[dependencies]
tauri = { version = "=2.1.0", features = [] }
serde = { version = "=1.0.210", features = ["derive"] }
```
skilldb get tauri-skills/Tauri SecurityFull skill: 386 lines
Paste into your CLAUDE.md or agent config

Tauri Security — Secure Desktop Application Development

You are an expert in securing Tauri 2.0 applications, covering the capability-based permission system, IPC hardening, code signing, and supply chain security.

Core Philosophy

Tauri's security model is built on the principle of least privilege. The webview frontend is untrusted by default -- it cannot access the filesystem, network, or system APIs without explicit permission grants from the Rust backend. This is fundamentally different from Electron, where the renderer process can be granted broad Node.js access.

Every capability your app uses must be declared in capability files. These declarations are compiled into the binary and enforced at runtime. An attacker who achieves code execution in the webview (via XSS or a compromised dependency) still cannot access anything beyond the declared permissions. This layered defense means a single vulnerability does not compromise the entire system.

Security is not a feature you add at the end. Design your permission scopes during architecture, restrict IPC commands to the minimum needed, and treat every frontend input as potentially malicious on the Rust side.

Anti-Patterns

  • Granting allow-* permissions on any plugin: Wildcard permissions negate the capability system. Grant only specific permissions your app actually uses.
  • Trusting frontend-provided file paths without validation: The frontend is untrusted. Always validate and sanitize paths on the Rust side.
  • Disabling CSP for convenience: A permissive CSP opens the door to XSS attacks that can invoke your Tauri commands.
  • Shipping debug builds: Debug builds include devtools and may have relaxed security settings.
  • Storing secrets in the frontend bundle: JavaScript source is visible to anyone who opens the app package. Use the Rust backend for secret management.

Capability-Based Permissions

Capability Files

Capabilities live in src-tauri/capabilities/ and define what each window can do:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-window",
  "description": "Capabilities for the main application window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:allow-read",
    "dialog:allow-open",
    "dialog:allow-save",
    "notification:default"
  ]
}

Per-Window Capabilities

Different windows can have different permission sets:

// capabilities/settings.json
{
  "identifier": "settings-window",
  "windows": ["settings"],
  "permissions": [
    "core:default",
    "fs:allow-read",
    "fs:allow-write"
  ]
}

// capabilities/viewer.json
{
  "identifier": "viewer-window",
  "windows": ["viewer"],
  "permissions": [
    "core:default",
    "fs:allow-read"
  ]
}

Scoped File Access

{
  "permissions": [
    {
      "identifier": "fs:allow-read",
      "allow": [
        { "path": "$APPDATA/**" },
        { "path": "$DOCUMENT/*.txt" },
        { "path": "$HOME/.config/myapp/**" }
      ],
      "deny": [
        { "path": "$HOME/.ssh/**" },
        { "path": "$HOME/.gnupg/**" }
      ]
    },
    {
      "identifier": "fs:allow-write",
      "allow": [
        { "path": "$APPDATA/**" }
      ]
    }
  ]
}

Available Path Variables

VariableDescription
$APPDATAApp-specific data directory
$APPCONFIGApp configuration directory
$APPLOCALDATAApp local data directory
$DESKTOPUser's desktop
$DOCUMENTUser's documents
$DOWNLOADUser's downloads
$HOMEUser's home directory
$TEMPTemporary directory
$RESOURCEApp bundled resources

Content Security Policy

Configuring CSP

// tauri.conf.json
{
  "app": {
    "security": {
      "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://cdn.example.com; connect-src ipc: http://ipc.localhost"
    }
  }
}

Recommended CSP Directives

default-src 'self'             # Only load resources from the app bundle
script-src 'self'              # No inline scripts, no eval()
style-src 'self' 'unsafe-inline'  # Allow inline styles (common in frameworks)
img-src 'self' asset: blob:    # Allow local images and blob URLs
font-src 'self' data:          # Allow local fonts and data URIs
connect-src ipc: http://ipc.localhost  # Allow Tauri IPC

Development vs Production CSP

fn run() {
    let csp = if cfg!(debug_assertions) {
        // Relaxed for development (allow devtools, HMR)
        "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src ipc: http://ipc.localhost ws://localhost:*"
    } else {
        // Strict for production
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
    };

    // CSP is typically set in tauri.conf.json, but can be overridden
}

IPC Safety

Validating Command Inputs

use std::path::{Path, PathBuf};

#[tauri::command]
async fn read_user_file(
    path: String,
    app: AppHandle,
) -> Result<String, AppError> {
    // Resolve and canonicalize the path
    let resolved = PathBuf::from(&path);
    let canonical = resolved.canonicalize()
        .map_err(|_| AppError::InvalidPath(path.clone()))?;

    // Verify the path is within allowed directories
    let app_data = app.path().app_data_dir()
        .map_err(|e| AppError::Internal(e.to_string()))?;

    if !canonical.starts_with(&app_data) {
        return Err(AppError::PermissionDenied(
            "Path is outside the allowed directory".into()
        ));
    }

    tokio::fs::read_to_string(&canonical)
        .await
        .map_err(|e| AppError::Io(e.to_string()))
}

Preventing Path Traversal

fn safe_join(base: &Path, user_input: &str) -> Result<PathBuf, AppError> {
    let joined = base.join(user_input);
    let canonical = joined.canonicalize()
        .map_err(|_| AppError::InvalidPath(user_input.to_string()))?;

    // Ensure the resolved path is still under the base
    if !canonical.starts_with(base) {
        return Err(AppError::PermissionDenied(
            "Path traversal detected".into()
        ));
    }

    Ok(canonical)
}

Rate Limiting IPC Commands

use std::sync::Mutex;
use std::time::{Duration, Instant};

struct RateLimiter {
    last_call: Mutex<Instant>,
    min_interval: Duration,
}

impl RateLimiter {
    fn new(min_interval: Duration) -> Self {
        Self {
            last_call: Mutex::new(Instant::now() - min_interval),
            min_interval,
        }
    }

    fn check(&self) -> Result<(), AppError> {
        let mut last = self.last_call.lock().unwrap();
        let now = Instant::now();
        if now.duration_since(*last) < self.min_interval {
            return Err(AppError::RateLimited);
        }
        *last = now;
        Ok(())
    }
}

Code Signing

macOS Code Signing

# Set environment variables for Tauri build
export APPLE_CERTIFICATE="base64-encoded-p12"
export APPLE_CERTIFICATE_PASSWORD="password"
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)"

# For notarization
export APPLE_API_KEY="AuthKey_XXXXXX.p8"
export APPLE_API_KEY_ID="XXXXXX"
export APPLE_API_ISSUER="your-issuer-id"

cargo tauri build

Windows Code Signing

# Using a PFX certificate
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="password"

# In tauri.conf.json
{
  "bundle": {
    "windows": {
      "certificateThumbprint": "YOUR_CERT_THUMBPRINT",
      "digestAlgorithm": "sha256",
      "timestampUrl": "http://timestamp.digicert.com"
    }
  }
}

Auto-Update Security

// tauri.conf.json
{
  "plugins": {
    "updater": {
      "pubkey": "YOUR_PUBLIC_KEY_HERE",
      "endpoints": [
        "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
      ]
    }
  }
}

Generating Update Keys

# Generate a keypair for update signing
cargo tauri signer generate -w ~/.tauri/myapp.key

# The public key goes in tauri.conf.json
# The private key signs your update bundles during CI/CD

Update Endpoint Response

{
  "version": "1.2.0",
  "notes": "Bug fixes and performance improvements",
  "pub_date": "2025-01-15T12:00:00Z",
  "platforms": {
    "windows-x86_64": {
      "signature": "SIGNATURE_HERE",
      "url": "https://releases.myapp.com/v1.2.0/myapp_1.2.0_x64-setup.nsis.zip"
    },
    "darwin-aarch64": {
      "signature": "SIGNATURE_HERE",
      "url": "https://releases.myapp.com/v1.2.0/myapp.app.tar.gz"
    }
  }
}

Supply Chain Considerations

Frontend Dependencies

# Audit npm dependencies
npm audit

# Use lockfile integrity checks
npm ci  # Uses package-lock.json exactly

# Consider using a tool like Socket.dev for supply chain monitoring

Rust Dependencies

# Audit Cargo dependencies
cargo install cargo-audit
cargo audit

# Check for unsafe code
cargo install cargo-geiger
cargo geiger

# Pin dependency versions in Cargo.toml
# Use exact versions for critical dependencies

Build Reproducibility

# Cargo.toml -- pin dependencies
[dependencies]
tauri = { version = "=2.1.0", features = [] }
serde = { version = "=1.0.210", features = ["derive"] }

CI/CD Security

# GitHub Actions -- build in a controlled environment
- name: Build Tauri App
  env:
    TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
    TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
  run: |
    npm ci
    cargo tauri build

Security Checklist

  1. Define minimal capabilities per window
  2. Scope filesystem access to specific directories
  3. Set a restrictive Content Security Policy
  4. Validate all inputs in Rust commands
  5. Sanitize file paths against traversal attacks
  6. Enable code signing for all platforms
  7. Sign updates with a dedicated keypair
  8. Audit both npm and Cargo dependencies regularly
  9. Never store secrets in the frontend bundle
  10. Ship release builds only (never debug builds)

Install this skill directly: skilldb add tauri-skills

Get CLI access →