Security
Desktop application security including context isolation, preload scripts, CSP, and sandboxing for Electron and Tauri
You are an expert in securing desktop applications, covering Electron's context isolation and sandboxing model, Tauri's permission system, Content Security Policy, and defense against common desktop app vulnerabilities. ## Key Points 1. **Context isolation**: Separates preload script context from renderer page context 2. **Sandbox**: Restricts renderer process capabilities at the OS level 3. **Preload scripts**: Controlled bridge exposing only necessary APIs 4. **Content Security Policy (CSP)**: Restricts what resources the renderer can load and execute 1. **Rust backend**: Memory-safe language eliminates entire classes of vulnerabilities 2. **Permission system**: Capability-based access control per window 3. **Command allowlisting**: Only explicitly registered commands are callable 4. **CSP**: Configurable policy for the webview - Enable `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: true` on every `BrowserWindow`. These are the foundation of Electron security. - Never expose raw `ipcRenderer`, `require`, `process`, or `__dirname` to the renderer. Only expose specific, validated function calls. - Validate every argument received in IPC handlers. Treat the renderer as an untrusted client. - Implement path traversal protection on all file operations. Use `path.resolve()` and verify the result starts with an allowed directory.
skilldb get desktop-app-skills/SecurityFull skill: 426 linesDesktop App Security — Desktop Apps
You are an expert in securing desktop applications, covering Electron's context isolation and sandboxing model, Tauri's permission system, Content Security Policy, and defense against common desktop app vulnerabilities.
Overview
Desktop applications face unique security challenges compared to web apps. They run with elevated privileges, have file system access, and can execute system commands. A vulnerability in a desktop app can lead to full system compromise. Both Electron and Tauri provide layered security models, but they must be correctly configured.
Electron's security layers:
- Context isolation: Separates preload script context from renderer page context
- Sandbox: Restricts renderer process capabilities at the OS level
- Preload scripts: Controlled bridge exposing only necessary APIs
- Content Security Policy (CSP): Restricts what resources the renderer can load and execute
Tauri's security layers:
- Rust backend: Memory-safe language eliminates entire classes of vulnerabilities
- Permission system: Capability-based access control per window
- Command allowlisting: Only explicitly registered commands are callable
- CSP: Configurable policy for the webview
Setup & Configuration
Electron: Secure BrowserWindow defaults
const mainWindow = new BrowserWindow({
webPreferences: {
// CRITICAL: Always enable these
contextIsolation: true, // Separate preload and page contexts
nodeIntegration: false, // No Node.js in renderer
sandbox: true, // OS-level sandboxing
// Important security settings
preload: path.join(__dirname, 'preload.js'),
webSecurity: true, // Enforce same-origin policy
allowRunningInsecureContent: false,
enableWebSQL: false,
// Disable features you don't need
webviewTag: false, // Disable <webview> tag
navigateOnDragDrop: false, // Prevent file drag navigation
},
});
Electron: Content Security Policy
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.yourapp.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
">
// Or set CSP via response headers in main process
const { session } = require('electron');
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
],
},
});
});
Tauri: Security configuration
// tauri.conf.json
{
"app": {
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost",
"freezePrototype": true,
"dangerousDisableAssetCspModification": false
}
}
}
Tauri: Capability-based permissions
// src-tauri/capabilities/main-window.json
{
"identifier": "main-window",
"description": "Permissions for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-open",
"dialog:allow-save",
{
"identifier": "fs:allow-read",
"allow": [{ "path": "$DOCUMENT/**" }]
},
{
"identifier": "fs:allow-write",
"allow": [{ "path": "$DOCUMENT/**" }]
},
{
"identifier": "http:default",
"allow": [{ "url": "https://api.yourapp.com/**" }]
}
]
}
// src-tauri/capabilities/settings-window.json — more restricted
{
"identifier": "settings-window",
"windows": ["settings"],
"permissions": [
"core:default"
]
}
Core Patterns
Electron: Secure preload script
// preload.js — minimal, validated API surface
const { contextBridge, ipcRenderer } = require('electron');
// Only expose specific, validated operations
contextBridge.exposeInMainWorld('api', {
// Typed operations instead of generic IPC
readFile: (path) => ipcRenderer.invoke('secure:read-file', path),
saveFile: (path, content) => ipcRenderer.invoke('secure:save-file', path, content),
getSettings: () => ipcRenderer.invoke('secure:get-settings'),
setSetting: (key, value) => ipcRenderer.invoke('secure:set-setting', key, value),
// Event subscriptions with cleanup
onUpdateAvailable: (callback) => {
const handler = (_event, info) => callback(info);
ipcRenderer.on('update:available', handler);
return () => ipcRenderer.removeListener('update:available', handler);
},
});
// NEVER do this:
// contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer);
// contextBridge.exposeInMainWorld('require', require);
// contextBridge.exposeInMainWorld('process', process);
Electron: Input validation in IPC handlers
// main.js — validate ALL inputs from renderer
const { ipcMain } = require('electron');
const path = require('node:path');
const fsp = require('node:fs/promises');
const ALLOWED_BASE_DIR = app.getPath('documents');
function isPathWithinAllowed(filePath, baseDir) {
const resolved = path.resolve(filePath);
return resolved.startsWith(path.resolve(baseDir) + path.sep);
}
ipcMain.handle('secure:read-file', async (_event, filePath) => {
// Validate type
if (typeof filePath !== 'string') {
throw new Error('Invalid file path');
}
// Prevent path traversal
if (!isPathWithinAllowed(filePath, ALLOWED_BASE_DIR)) {
throw new Error('Access denied: path outside allowed directory');
}
// Validate extension
const ext = path.extname(filePath).toLowerCase();
const allowedExtensions = ['.txt', '.md', '.json', '.csv'];
if (!allowedExtensions.includes(ext)) {
throw new Error(`File type not allowed: ${ext}`);
}
return fsp.readFile(filePath, 'utf-8');
});
ipcMain.handle('secure:set-setting', async (_event, key, value) => {
// Validate setting key against allowlist
const allowedKeys = ['theme', 'language', 'fontSize', 'autoSave'];
if (!allowedKeys.includes(key)) {
throw new Error(`Unknown setting: ${key}`);
}
// Validate value types
const validators = {
theme: (v) => ['light', 'dark', 'system'].includes(v),
language: (v) => /^[a-z]{2}(-[A-Z]{2})?$/.test(v),
fontSize: (v) => typeof v === 'number' && v >= 8 && v <= 72,
autoSave: (v) => typeof v === 'boolean',
};
if (!validators[key]?.(value)) {
throw new Error(`Invalid value for setting ${key}`);
}
await updateSettings(key, value);
});
Electron: Navigation and new window restrictions
// Prevent navigation to untrusted URLs
mainWindow.webContents.on('will-navigate', (event, url) => {
const parsedUrl = new URL(url);
const allowedOrigins = ['http://localhost:3000', 'file:'];
if (!allowedOrigins.some((origin) => url.startsWith(origin))) {
event.preventDefault();
console.warn('Blocked navigation to:', url);
}
});
// Prevent new window creation (popups, window.open)
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Open external links in the system browser
if (url.startsWith('https://')) {
require('electron').shell.openExternal(url);
}
return { action: 'deny' };
});
Electron: Secure protocol handlers
// Register a custom protocol for loading local resources
const { protocol } = require('electron');
protocol.handle('app', (request) => {
const url = new URL(request.url);
const filePath = path.join(__dirname, 'renderer', url.pathname);
// Prevent path traversal via the protocol
const resolved = path.resolve(filePath);
if (!resolved.startsWith(path.resolve(__dirname, 'renderer'))) {
return new Response('Forbidden', { status: 403 });
}
return net.fetch(`file://${resolved}`);
});
Protecting stored data
// Electron: Use safeStorage for encrypting sensitive data
const { safeStorage } = require('electron');
function encryptSecret(plaintext) {
if (safeStorage.isEncryptionAvailable()) {
const encrypted = safeStorage.encryptString(plaintext);
return encrypted.toString('base64');
}
throw new Error('Encryption not available on this system');
}
function decryptSecret(encryptedBase64) {
if (safeStorage.isEncryptionAvailable()) {
const buffer = Buffer.from(encryptedBase64, 'base64');
return safeStorage.decryptString(buffer);
}
throw new Error('Encryption not available on this system');
}
// Usage: storing an API key
const encrypted = encryptSecret('sk-my-secret-api-key');
store.set('apiKey', encrypted);
// Retrieving
const apiKey = decryptSecret(store.get('apiKey'));
Permission request handling
// Control which permissions the renderer can request
session.defaultSession.setPermissionRequestHandler(
(webContents, permission, callback) => {
const allowedPermissions = ['clipboard-read', 'notifications'];
if (allowedPermissions.includes(permission)) {
callback(true);
} else {
console.warn(`Denied permission request: ${permission}`);
callback(false);
}
}
);
// Control which permission checks return true
session.defaultSession.setPermissionCheckHandler(
(_webContents, permission) => {
const allowedPermissions = ['clipboard-read', 'notifications'];
return allowedPermissions.includes(permission);
}
);
Tauri: Secure command design
use std::path::{Path, PathBuf};
use tauri::Manager;
// Validate paths in Rust commands
#[tauri::command]
async fn read_user_file(
app: tauri::AppHandle,
relative_path: String,
) -> Result<String, String> {
// Construct path within allowed directory
let docs_dir = app.path().document_dir()
.map_err(|e| format!("Cannot resolve documents dir: {e}"))?;
let full_path = docs_dir.join(&relative_path);
// Prevent path traversal
let canonical = full_path.canonicalize()
.map_err(|e| format!("Invalid path: {e}"))?;
if !canonical.starts_with(&docs_dir) {
return Err("Access denied: path outside documents directory".into());
}
std::fs::read_to_string(&canonical)
.map_err(|e| format!("Failed to read file: {e}"))
}
// Use strong types instead of raw strings
#[derive(serde::Deserialize)]
struct CreateItemRequest {
name: String,
description: String,
}
#[tauri::command]
async fn create_item(request: CreateItemRequest) -> Result<String, String> {
// Validate input lengths
if request.name.is_empty() || request.name.len() > 255 {
return Err("Name must be 1-255 characters".into());
}
if request.description.len() > 10_000 {
return Err("Description too long".into());
}
// Sanitize — strip control characters
let name: String = request.name.chars()
.filter(|c| !c.is_control())
.collect();
// Process the validated input
Ok(format!("Created: {name}"))
}
Best Practices
- Enable
contextIsolation: true,nodeIntegration: false, andsandbox: trueon everyBrowserWindow. These are the foundation of Electron security. - Never expose raw
ipcRenderer,require,process, or__dirnameto the renderer. Only expose specific, validated function calls. - Validate every argument received in IPC handlers. Treat the renderer as an untrusted client.
- Implement path traversal protection on all file operations. Use
path.resolve()and verify the result starts with an allowed directory. - Set a strict Content Security Policy that blocks
eval(), inline scripts, and unauthorized remote resources. - Block navigation to untrusted URLs and prevent new window creation from the renderer.
- Use
safeStorage(Electron) or OS keychain libraries to encrypt stored secrets. Never store API keys or tokens in plaintext config files. - In Tauri, define the narrowest possible permissions per window. A settings window should not have file system access.
- Regularly update Electron and Tauri to get Chromium/WebKit security patches.
Common Pitfalls
- Disabling
contextIsolationfor convenience: This gives the renderer page direct access to Node.js APIs through the preload. Any XSS vulnerability becomes a full system compromise. - Using
shell.openExternalwith unvalidated URLs: An attacker-controlled URL passed toshell.openExternalcan execute arbitrary commands on some platforms. Always validate the URL scheme. - Storing secrets in
localStorageor plain JSON files: These are trivially readable. Use OS-provided encryption (safeStorage, Keychain, DPAPI). - Loading remote content without CSP: If your app loads any remote content, an attacker who compromises that origin gains access to all exposed APIs.
- Ignoring the
will-navigateevent: Without navigation restrictions, an attacker can navigate the renderer to a malicious page that still has access to your preload APIs. - Over-permissive Tauri capabilities: Granting
fs:allow-readwithout a scope restriction gives access to the entire file system. - Not updating dependencies: Electron bundles Chromium, which receives frequent security patches. Running an outdated Electron version exposes users to known browser vulnerabilities.
- Prototype pollution: In Electron, set
contextIsolation: trueto prevent page scripts from modifyingObject.prototypeand affecting the preload. In Tauri, enablefreezePrototype: true.
Core Philosophy
Desktop application security operates on a fundamentally different threat model than web security. A vulnerability in a web application might leak data or deface a page. A vulnerability in a desktop application can install malware, exfiltrate files, or compromise the entire operating system. The renderer process is a browser window with potential access to the full filesystem and process execution — every security control must be designed with this gravity in mind.
Defense in depth is the only viable strategy. No single control is sufficient. Context isolation prevents the page from accessing Node.js APIs. Sandboxing restricts the renderer at the OS level. Preload scripts expose only specific, validated functions. Content Security Policy limits what resources can load. Input validation catches malicious data. Each layer catches what the others miss.
Treat the renderer as untrusted, always. Even if you only load local HTML files, a future feature might load remote content, render user-supplied HTML, or integrate a third-party widget. Building every IPC handler with input validation, path traversal protection, and type checking from day one means you are protected when the attack surface inevitably expands.
Anti-Patterns
-
Disabling
contextIsolationfor convenience — this gives the renderer page direct access to Node.js APIs through the preload; any XSS vulnerability becomes a full system compromise. -
Exposing
require,process, oripcRendererdirectly — passing these objects throughcontextBridgeto the renderer gives the page unlimited access to the system; only expose specific, validated function wrappers. -
Storing secrets in plaintext configuration files — API keys, tokens, and passwords in JSON config files are trivially readable; use
safeStorage(Electron) or OS keychain integration for encrypted storage. -
Accepting file paths from the renderer without validation — a renderer-supplied path like
../../../etc/passwdcan escape allowed directories; always resolve paths and verify they start with the allowed base directory. -
Not restricting navigation and window creation — without
will-navigateandsetWindowOpenHandlerrestrictions, a compromised renderer can navigate to a malicious page that retains access to all preload APIs.
Install this skill directly: skilldb add desktop-app-skills
Related Skills
Auto Update
Auto-update mechanisms for desktop applications using electron-updater and Tauri's built-in updater
Electron Ipc
IPC communication patterns for Electron main and renderer process messaging
Electron
Electron fundamentals for building cross-platform desktop applications with web technologies
File System Access
File system access patterns, native dialogs, and drag-and-drop for desktop applications
Native Menus
System tray icons, native application menus, and desktop notifications for Electron and Tauri apps
Packaging
Building, packaging, and distributing desktop applications for Windows, macOS, and Linux