Tauri 2.0 Security Content Security Policy and Permissions

Security in Tauri 2.0 represents fundamental design philosophy protecting users from malicious code, unauthorized access, and data breaches through multi-layered security model including Content Security Policy (CSP) restricting resource loading, allowlist permissions limiting API access, process isolation separating frontend from backend, API scoping preventing unauthorized operations, secure IPC communication, and capability-based security ensuring only explicitly permitted actions execute. Tauri's security architecture surpasses Electron by default requiring explicit permission grants for filesystem access, network requests, shell commands, and system APIs preventing supply chain attacks, XSS vulnerabilities, and unauthorized data access while maintaining developer flexibility building powerful desktop applications. This comprehensive guide covers understanding Tauri's security model and threat mitigation, configuring Content Security Policy restricting scripts and resources, implementing allowlist permissions for filesystem, HTTP, shell, and window APIs, securing IPC communication with validation and sanitization, protecting API keys and sensitive data, handling user input safely preventing injection attacks, implementing secure update mechanisms, auditing dependencies and supply chain security, following security best practices for production applications, and building secure real-world applications with proper authentication, encryption, and data protection. Mastering security patterns enables building trustworthy desktop applications protecting user data, preventing malware, maintaining privacy, and establishing user confidence through transparent security practices and regular security audits. Before proceeding, understand command creation and file system operations.
Tauri Security Architecture
| Security Layer | Protection | Default State | Configuration |
|---|---|---|---|
| Content Security Policy | Prevents XSS, script injection | Strict | tauri.conf.json CSP |
| Allowlist Permissions | Limits API access | Deny all | tauri.conf.json allowlist |
| Process Isolation | Separates UI and backend | Enabled | Built-in |
| API Scoping | Restricts file/path access | Restricted | Scope patterns |
| IPC Validation | Validates command inputs | Type-checked | Rust type system |
| HTTPS Enforcement | Secure remote content | Required | CSP directives |
Content Security Policy
// src-tauri/tauri.conf.json
// Content Security Policy Configuration
{
"tauri": {
"security": {
"csp": {
// Strict CSP (recommended for production)
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https:",
"font-src": "'self' data:",
"connect-src": "'self' https://api.example.com",
"object-src": "'none'",
"base-uri": "'self'",
"form-action": "'self'",
"frame-ancestors": "'none'",
"upgrade-insecure-requests": true
},
// Development CSP (more permissive)
"devCsp": {
"default-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"connect-src": "'self' ws://localhost:*"
}
}
}
}
// Recommended Production CSP
{
"tauri": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
}
}
}
// CSP for Single Page Applications with external APIs
{
"tauri": {
"security": {
"csp": {
"default-src": "'self'",
"script-src": ["'self'", "'wasm-unsafe-eval'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "https:"],
"connect-src": [
"'self'",
"https://api.github.com",
"https://api.example.com"
]
}
}
}
}
// CSP Directives Explained:
// - default-src: Fallback for all fetch directives
// - script-src: Controls JavaScript execution
// - style-src: Controls CSS loading
// - img-src: Controls image loading
// - font-src: Controls font loading
// - connect-src: Controls network requests (fetch, XHR, WebSocket)
// - object-src: Controls plugins (Flash, etc.)
// - base-uri: Restricts <base> tag URLs
// - form-action: Restricts form submission targets
// - frame-ancestors: Prevents embedding in iframes
// CSP Values:
// - 'self': Same origin only
// - 'none': Block all
// - 'unsafe-inline': Allow inline scripts/styles (avoid in production)
// - 'unsafe-eval': Allow eval() (avoid in production)
// - https:: All HTTPS URLs
// - data:: Data URIs
// - wasm-unsafe-eval: Allow WebAssemblyAllowlist Permissions
// src-tauri/tauri.conf.json
// Allowlist Configuration
{
"tauri": {
"allowlist": {
// Global permission - avoid using "all: true" in production
"all": false,
// File System permissions
"fs": {
"all": false,
"readFile": true,
"writeFile": true,
"readDir": true,
"createDir": true,
"removeDir": false,
"removeFile": false,
"copyFile": true,
"renameFile": true,
"exists": true,
"scope": [
"$APPDATA/**",
"$DOCUMENT/**",
"$DOWNLOAD/**"
]
},
// HTTP permissions
"http": {
"all": false,
"request": true,
"scope": [
"https://api.example.com/**",
"https://api.github.com/**"
]
},
// Dialog permissions
"dialog": {
"all": false,
"open": true,
"save": true,
"message": true,
"ask": true,
"confirm": true
},
// Shell permissions (be very careful!)
"shell": {
"all": false,
"execute": false,
"open": true,
"scope": [
{
"name": "open-browser",
"cmd": "open",
"args": ["https://**"]
}
]
},
// Window permissions
"window": {
"all": false,
"create": true,
"center": true,
"requestUserAttention": true,
"setResizable": true,
"setTitle": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"show": true,
"hide": true,
"close": true,
"setDecorations": true,
"setAlwaysOnTop": true,
"setSize": true,
"setPosition": true,
"setFullscreen": true,
"setFocus": true,
"setIcon": true,
"setSkipTaskbar": true,
"startDragging": true,
"print": false
},
// Clipboard permissions
"clipboard": {
"all": false,
"readText": true,
"writeText": true
},
// Global shortcut permissions
"globalShortcut": {
"all": false
},
// Notification permissions
"notification": {
"all": true
},
// Path permissions
"path": {
"all": true
},
// Protocol permissions
"protocol": {
"all": false,
"asset": true,
"assetScope": ["$APPDATA/**"]
}
}
}
}
// Minimal Production Allowlist (Most Secure)
{
"tauri": {
"allowlist": {
"all": false,
"fs": {
"scope": ["$APPDATA/**"]
},
"dialog": {
"open": true,
"save": true
},
"window": {
"create": true,
"close": true
}
}
}
}
// Scope Variables:
// $APPDATA - Application data directory
// $APPCONFIG - Application config directory
// $APPLOG - Application log directory
// $AUDIO - User's audio directory
// $CACHE - User's cache directory
// $DATA - User's data directory
// $DESKTOP - User's desktop directory
// $DOCUMENT - User's document directory
// $DOWNLOAD - User's download directory
// $HOME - User's home directory
// $PICTURE - User's picture directory
// $PUBLIC - User's public directory
// $RUNTIME - Runtime directory
// $TEMP - Temporary directory
// $VIDEO - User's video directory
// ** - Recursive wildcard
// * - Single level wildcardSecure IPC Communication
// Rust: Secure IPC with validation
use serde::{Deserialize, Serialize};
// Input validation
#[derive(Deserialize)]
struct UserInput {
name: String,
email: String,
}
// Validate and sanitize inputs
fn validate_email(email: &str) -> Result<(), String> {
if !email.contains('@') || email.len() > 254 {
return Err("Invalid email format".to_string());
}
Ok(())
}
fn sanitize_string(input: &str) -> String {
// Remove potentially dangerous characters
input
.chars()
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '@' || *c == '.')
.collect()
}
#[tauri::command]
fn create_user_secure(input: UserInput) -> Result<String, String> {
// Validate input length
if input.name.is_empty() || input.name.len() > 100 {
return Err("Invalid name length".to_string());
}
// Validate email
validate_email(&input.email)?;
// Sanitize inputs
let safe_name = sanitize_string(&input.name);
let safe_email = sanitize_string(&input.email);
// Process with sanitized data
Ok(format!("Created user: {} ({})", safe_name, safe_email))
}
// Path traversal prevention
use std::path::{Path, PathBuf};
#[tauri::command]
fn read_file_secure(app: tauri::AppHandle, filename: String) -> Result<String, String> {
// Get app data directory
let app_dir = app.path_resolver()
.app_data_dir()
.ok_or("Failed to get app directory")?;
// Construct full path
let file_path = app_dir.join(&filename);
// Verify path is within app directory (prevent path traversal)
if !file_path.starts_with(&app_dir) {
return Err("Invalid file path - path traversal attempt detected".to_string());
}
// Verify file exists and is a file (not a directory)
if !file_path.is_file() {
return Err("File not found or is a directory".to_string());
}
// Read file
std::fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file: {}", e))
}
// SQL injection prevention with prepared statements
use rusqlite::{params, Connection};
#[tauri::command]
fn query_user_secure(db: tauri::State<DbConnection>, user_id: i64) -> Result<User, String> {
let conn = db.conn.lock().unwrap();
// Use prepared statement with parameter binding
let mut stmt = conn
.prepare("SELECT id, name, email FROM users WHERE id = ?1")
.map_err(|e| e.to_string())?;
let user = stmt
.query_row(params![user_id], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
})
})
.map_err(|e| e.to_string())?;
Ok(user)
}
// Command injection prevention
#[tauri::command]
async fn execute_safe_command(command: String, args: Vec<String>) -> Result<String, String> {
// Whitelist allowed commands
let allowed_commands = vec!["git", "npm", "cargo"];
if !allowed_commands.contains(&command.as_str()) {
return Err("Command not allowed".to_string());
}
// Validate arguments (no shell metacharacters)
for arg in &args {
if arg.contains(['&', '|', ';', '$', '`', '\n', '\r']) {
return Err("Invalid argument - shell metacharacters detected".to_string());
}
}
// Execute safely
let output = tokio::process::Command::new(&command)
.args(&args)
.output()
.await
.map_err(|e| e.to_string())?;
String::from_utf8(output.stdout)
.map_err(|e| e.to_string())
}
// Rate limiting
use std::sync::Mutex;
use std::collections::HashMap;
use std::time::{Duration, Instant};
struct RateLimiter {
requests: Mutex<HashMap<String, Vec<Instant>>>,
}
impl RateLimiter {
fn check_rate_limit(&self, key: &str, max_requests: usize, window: Duration) -> bool {
let mut requests = self.requests.lock().unwrap();
let now = Instant::now();
// Get or create request history
let history = requests.entry(key.to_string()).or_insert_with(Vec::new);
// Remove old requests outside the window
history.retain(|&time| now.duration_since(time) < window);
// Check if limit exceeded
if history.len() >= max_requests {
return false;
}
// Add current request
history.push(now);
true
}
}
#[tauri::command]
fn rate_limited_operation(
limiter: tauri::State<RateLimiter>,
user_id: String
) -> Result<String, String> {
// Allow 10 requests per minute
if !limiter.check_rate_limit(&user_id, 10, Duration::from_secs(60)) {
return Err("Rate limit exceeded. Please try again later.".to_string());
}
// Process request
Ok("Operation successful".to_string())
}Protecting Sensitive Data
// Rust: Protecting sensitive data
// Store API keys securely (never in frontend)
use std::env;
struct ApiCredentials {
api_key: String,
api_secret: String,
}
impl ApiCredentials {
fn from_env() -> Result<Self, String> {
Ok(ApiCredentials {
api_key: env::var("API_KEY")
.map_err(|_| "API_KEY not set".to_string())?,
api_secret: env::var("API_SECRET")
.map_err(|_| "API_SECRET not set".to_string())?,
})
}
}
// Use credentials in backend only
#[tauri::command]
async fn fetch_protected_data(
credentials: tauri::State<'_, ApiCredentials>
) -> Result<String, String> {
let client = reqwest::Client::new();
let response = client
.get("https://api.example.com/data")
.header("X-API-Key", &credentials.api_key)
.header("X-API-Secret", &credentials.api_secret)
.send()
.await
.map_err(|e| e.to_string())?;
response.text().await.map_err(|e| e.to_string())
}
// Encrypt sensitive data at rest
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use rand::Rng;
fn encrypt_data(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
let cipher = Aes256Gcm::new(key.into());
let nonce_bytes: [u8; 12] = rand::thread_rng().gen();
let nonce = Nonce::from_slice(&nonce_bytes);
let mut ciphertext = cipher
.encrypt(nonce, data)
.map_err(|e| format!("Encryption failed: {}", e))?;
// Prepend nonce to ciphertext
let mut result = nonce_bytes.to_vec();
result.append(&mut ciphertext);
Ok(result)
}
fn decrypt_data(encrypted: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
if encrypted.len() < 12 {
return Err("Invalid encrypted data".to_string());
}
let cipher = Aes256Gcm::new(key.into());
let (nonce_bytes, ciphertext) = encrypted.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
cipher
.decrypt(nonce, ciphertext)
.map_err(|e| format!("Decryption failed: {}", e))
}
// Secure credential storage using OS keyring
#[cfg(feature = "keyring")]
use keyring::Entry;
#[tauri::command]
fn store_credential_secure(service: String, username: String, password: String) -> Result<(), String> {
let entry = Entry::new(&service, &username)
.map_err(|e| e.to_string())?;
entry.set_password(&password)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn retrieve_credential_secure(service: String, username: String) -> Result<String, String> {
let entry = Entry::new(&service, &username)
.map_err(|e| e.to_string())?;
entry.get_password()
.map_err(|e| e.to_string())
}
// Hash passwords before storage
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::{SaltString, rand_core::OsRng};
fn hash_password(password: &str) -> Result<String, String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| e.to_string())
}
fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| e.to_string())?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
// Initialize in main with secure state
fn main() {
let credentials = ApiCredentials::from_env()
.expect("Failed to load credentials");
tauri::Builder::default()
.manage(credentials)
.invoke_handler(tauri::generate_handler![
fetch_protected_data,
store_credential_secure,
retrieve_credential_secure,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Security Best Practices
- Minimize Allowlist: Only enable APIs actually needed by application avoiding unnecessary attack surface
- Strict CSP: Use strict Content Security Policy preventing XSS and code injection
- Validate All Input: Always validate and sanitize user input preventing injection attacks
- Use Prepared Statements: Never concatenate SQL queries using parameter binding instead
- Scope File Access: Restrict filesystem access to specific directories using scope patterns
- Encrypt Sensitive Data: Encrypt API keys, passwords, and sensitive data at rest
- Backend API Calls: Make authenticated API calls from Rust protecting credentials
- Rate Limiting: Implement rate limiting preventing abuse and DoS attacks
- Audit Dependencies: Regular security audits checking for vulnerable dependencies
- Update Regularly: Keep Tauri and dependencies updated with security patches
all: true in production allowlist! Always use minimum required permissions, validate all inputs, protect API keys in backend, and implement proper error handling. Security is not optional!Production Security Checklist
- Review and minimize allowlist permissions removing unused APIs
- Configure strict CSP removing unsafe-inline and unsafe-eval
- Validate all user input with proper sanitization
- Use prepared statements for all database queries
- Restrict file system access with scope patterns
- Store API keys and secrets in environment variables or secure storage
- Implement rate limiting on sensitive operations
- Enable HTTPS for all network requests
- Implement proper error handling avoiding information leakage
- Audit dependencies regularly using cargo audit
- Test with security tools including fuzzing and penetration testing
- Implement secure auto-update mechanism with signature verification
- Use code signing for application binaries
- Document security measures in README
- Establish security disclosure policy
Next Steps
- System Tray: Add system tray with tray integration
- Building: Package securely with build process
- File System: Secure file access with file operations
- HTTP Client: Protect API keys with secure requests
- Database: Secure data with SQLite integration
Conclusion
Mastering security in Tauri 2.0 enables building trustworthy desktop applications protecting users from malicious code, unauthorized access, data breaches, and privacy violations through comprehensive security model including strict CSP preventing XSS, minimal allowlist reducing attack surface, process isolation separating concerns, secure IPC with validation, encrypted sensitive data, and regular security audits maintaining robust protection. Tauri's security-first architecture provides superior protection compared to traditional desktop frameworks requiring explicit permission grants, preventing supply chain attacks through Rust's memory safety, and enforcing capability-based security where only permitted operations execute maintaining user trust and data integrity. Understanding security patterns including input validation preventing injection, prepared statements for SQL safety, path traversal prevention, API key protection, rate limiting, encryption for sensitive data, and regular dependency audits establishes foundation for professional desktop application development delivering secure reliable software protecting user data and privacy throughout operation. Your Tauri applications now possess comprehensive security capabilities enabling features like secure authentication, encrypted storage, protected API communication, and trustworthy update mechanisms delivering professional desktop experiences users can trust with their sensitive data and privacy!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


