Security and Capabilities
How Tauri v2 protects your application through capability files plugin permissions security configuration and Content Security Policy
Every Tauri application bridges two worlds: a web frontend running inside a system WebView, and a Rust backend with full access to the operating system. The security system that sits between them decides what the frontend is allowed to ask the backend to do. In Tauri v2, that system is built around capability files, permissions, and Content Security Policy.
A Tauri app that loads a remote script or accepts user-generated content in the WebView is one compromised dependency away from exposing the file system, network, or shell to an attacker. The capabilities system exists to make sure that even if frontend code is taken over, the damage stays contained to what you explicitly allowed.
Default Deny in v2:
Tauri v2 denies all operations by default. Unlike v1, where some APIs were accessible without explicit configuration, v2 requires every permission to be declared in a capability file. If you invoke a command and nothing happens, check whether the corresponding permission is granted.
How the Tauri v2 Security Model Works
The process model ensures that even if an attacker manages to execute arbitrary JavaScript in the webview, they are trapped in the frontend process. They cannot access the filesystem, spawn processes, or read memory outside what the webview allows, because those actions require IPC calls. The IPC bridge is guarded by the capabilities you defined in tauri.conf.json, meaning the attacker is restricted to exactly the permissions you granted the application.
The innermost layer is the command-level scope, where individual Rust commands validate their inputs against allowed ranges.
A request from the frontend travels through all three: CSP must allow the connection, a capability must grant the permission for that window, and the command must accept the specific arguments. If any layer rejects the request, it fails.
The capabilities system answers one question: which windows can call which commands? You answer it by writing capability files — JSON or TOML documents that list permissions and the windows they apply to. These files live in src-tauri/capabilities/ and are automatically picked up during the build.
What Capabilities Protect Against:
A well-configured capability set can minimize the impact of a frontend compromise, prevent accidental exposure of local system interfaces, and reduce the risk of privilege escalation from the WebView into the operating system. It cannot protect against malicious Rust code, intentionally lax scopes, or vulnerabilities in the system WebView itself.
Think of it as a bouncer at a club. Each window (guest) arrives with a label on it. The capability files are the guest list. A permission is a stamp on the guest's hand that lets them into a specific area. If a window's label is not on the list — or if the permission it needs is not stamped — the request is turned away at the door. The frontend code never reaches the Rust command.
Capability Files
A capability file is a JSON or TOML document that bundles a set of permissions and assigns them to one or more windows. Every Tauri v2 project should have at least one capability file, usually named default.json, in the src-tauri/capabilities/ directory.
Anatomy of a Capability File
A capability file has four required fields and two optional ones. The required fields are identifier (a unique name), description (what this capability covers), windows (which window labels get these permissions), and permissions (the list of granted permissions). The optional fields are platforms (restrict to specific operating systems) and remote (allow remote URLs to access these permissions).
Here is a typical capability file for the main window of a desktop application:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Default capabilities for the main application window",
"windows": ["main"],
"permissions": [
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default",
"core:window:allow-set-title"
]
}
The $schema field points to a JSON Schema file that Tauri generates during the build. It enables autocompletion in editors like VS Code — when you start typing a permission identifier, the editor shows you all valid options. The schema path is relative to the capability file, so ../gen/schemas/desktop-schema.json reaches up from capabilities/ into gen/schemas/.
Each permission in the list follows the format plugin:permission. The prefix core: refers to Tauri's built-in plugin system. core:window:default is a predefined set — it grants all the common window operations like minimize, maximize, and close. core:window:allow-set-title is an individual permission that unlocks only the setTitle command.
The "windows": ["main"] line is where the mapping happens. This capability only applies to windows with the label main. If your application opens a second window labeled settings, it does not inherit these permissions unless you add "settings" to the array — or create a separate capability file for it.
Wildcard Window Labels:
Using "*" as a window label grants the permissions to every window in the application, including ones created dynamically at runtime. This is convenient during development but widens the attack surface. In production, prefer listing windows explicitly.
Where Capability Files Live
The capabilities/ directory sits inside src-tauri/, alongside tauri.conf.json and the Rust source code. A typical project structure looks like this:
src-tauri/
├── Cargo.toml
├── capabilities/
│ ├── default.json
│ ├── desktop.json
│ └── mobile.json
├── src/
│ └── main.rs
├── tauri.conf.json
└── icons/
Every file in capabilities/ is automatically loaded at build time. You do not need to import or register them anywhere — Tauri discovers them. However, once you explicitly list capabilities in tauri.conf.json under app.security.capabilities, only the listed ones are used. This is the recommended approach for production.
Enabling Capabilities in tauri.conf.json
The tauri.conf.json file has a security.capabilities field that accepts an array of capability identifiers or inline capability objects. The identifiers must match the identifier field in the corresponding capability file:
{
"app": {
"security": {
"capabilities": ["main-capability", "desktop-capability"]
}
}
}
This tells Tauri: "build the app with only these two capability sets." Any capability files in the directory that are not listed here are ignored. If you omit the capabilities field entirely, all files in the directory are included — which is fine for getting started but less predictable as the project grows.
Inline capabilities are also supported for simpler setups:
{
"app": {
"security": {
"capabilities": [
{
"identifier": "inline-capability",
"description": "Defined directly in tauri.conf.json",
"windows": ["*"],
"permissions": ["core:window:default", "core:app:default"]
}
]
}
}
}
Inline definitions work the same as file-based ones. The tradeoff is organizational: inline capabilities keep everything in one file but become hard to manage once you have more than two or three windows with different permission sets. File-based capabilities are easier to review, diff, and audit.
Setting Up Your First Capability File
Step 1: Create the capabilities directory
Inside your src-tauri/ folder, create a new directory called capabilities:
mkdir src-tauri/capabilities
Step 2: Create a default capability file
Create a file named default.json inside the capabilities directory:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capabilities for the main window",
"windows": ["main"],
"permissions": [
"core:path:default",
"core:event:default",
"core:window:default",
"core:app:default",
"core:resources:default",
"core:menu:default",
"core:tray:default"
]
}
This grants all the default core plugin permissions — enough for a basic application that uses windows, events, menus, and system tray features.
Step 3: Reference the capability in tauri.conf.json
Open tauri.conf.json and add the security.capabilities field:
{
"app": {
"security": {
"capabilities": ["default"]
}
}
}
Step 4: Build and verify
Run the build to confirm everything is wired up:
cargo tauri build
If a permission is missing, Tauri will log a runtime error when the frontend tries to invoke an unauthorized command. You will see messages like permission not granted for command X in the console.
Verification Complete:
If the build succeeds and your frontend commands execute without permission errors, your capability system is correctly configured. You can confirm this by temporarily removing a permission from the list and observing that the corresponding command fails at runtime with a clear error message.
Platform-Specific Capabilities
Not every permission makes sense on every platform. A capability that grants access to the barcode scanner is useless on desktop; a capability for global shortcut registration has no equivalent on mobile. Tauri lets you restrict capabilities to specific platforms using the platforms field.
A desktop-only capability might look like this:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Permissions only available on desktop platforms",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"core:window:allow-set-title",
"global-shortcut:allow-register"
]
}
And a mobile-only counterpart:
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "mobile-capability",
"description": "Permissions only available on mobile platforms",
"windows": ["main"],
"platforms": ["iOS", "android"],
"permissions": [
"nfc:allow-scan",
"biometric:allow-authenticate",
"barcode-scanner:allow-scan"
]
}
When you build for Linux, macOS, or Windows, Tauri includes capabilities where platforms is unset (all platforms) and capabilities where platforms explicitly lists the target desktop OS. The mobile-only capability file is ignored entirely — its permissions do not appear in the binary.
If the platforms field is omitted, the capability applies to all targets. This is the right choice for core permissions like window management and event handling, which work identically across every platform.
Remote API Access
By default, Tauri commands are only callable from bundled code — HTML, JavaScript, and CSS that ship inside the application binary. If your app loads content from a remote URL (for example, a web-based dashboard hosted on your server), that content cannot invoke Tauri commands unless you explicitly grant remote access.
The remote field in a capability file opens specific URLs:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "remote-access",
"description": "Allow remote dashboard to access window controls",
"windows": ["main"],
"remote": {
"urls": ["https://dashboard.myapp.com", "https://*.myapp.dev"]
},
"permissions": [
"core:window:allow-set-title",
"core:window:allow-close"
]
}
The urls array supports wildcards in the subdomain position. https://*.myapp.dev matches https://app.myapp.dev, https://staging.myapp.dev, and any other subdomain. It does not match https://myapp.dev itself — that requires a separate entry.
Remote Access is a Deliberate Decision:
Granting remote API access means that if the remote server is compromised, the attacker can invoke the listed Tauri commands in your users' applications. Only grant the minimum set of permissions necessary, and prefer restrictive URL patterns over broad wildcards. A capability that allows "urls": ["https://*"] effectively removes the security boundary between your app and the entire internet.
Commands and the Build Script
Commands registered in Rust through tauri::Builder::invoke_handler are accessible from all windows by default. To restrict which commands are even available for capability files to reference, use the AppManifest in your build script:
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.app_manifest(
tauri_build::AppManifest::new()
.commands(&["read_file", "write_config", "fetch_data"])
),
)
.unwrap();
}
Only the commands listed in commands() are compiled as invocable. If a capability file grants a permission for a command not in this list, it has no effect — the command simply does not exist in the binary. This is a compile-time guard that catches misconfigurations before they reach production.
Plugin Permissions
Plugins in Tauri v2 ship with their own permission definitions. When you add a plugin like tauri-plugin-fs or tauri-plugin-shell, it contributes a set of permissions that you can reference in your capability files. Each plugin defines both a default permission set and individual permissions for specific operations.
How Plugin Permissions Are Structured
A plugin permission identifier has two parts separated by a colon: the plugin name and the permission name. For the filesystem plugin, fs:default grants all the common read and write operations. fs:allow-read-file grants only the ability to read a file. fs:deny-write-file explicitly blocks writing.
Plugin developers define these permissions in a permissions/ directory inside the plugin crate. When you build your application, Tauri collects all permissions from all plugins and generates the JSON Schema that powers IDE autocompletion.
Here is how a typical capability file references plugin permissions alongside core ones:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Main window with filesystem and dialog access",
"windows": ["main"],
"permissions": [
"core:window:default",
"core:app:default",
"fs:default",
"dialog:allow-open",
"dialog:allow-save"
]
}
The fs:default permission is a convenience bundle. It includes read and write access to the application's data directory, but it does not grant access to arbitrary paths. If the frontend tries to read /etc/passwd, the command fails — not because the permission is missing, but because the scope check inside the Rust command rejects the path.
Permission vs. Scope:
A permission grants the ability to call a command at all. A scope constrains what arguments that command accepts. fs:default gives permission to call readFile, but the scope — defined separately in the permission configuration — limits which paths are valid. Both must pass for the operation to succeed.
Permission Identifier Convention
Permissions follow a naming convention that makes their purpose obvious at a glance:
<plugin>:default— the recommended default set for that plugin<plugin>:allow-<operation>— grants access to a single operation<plugin>:deny-<operation>— explicitly blocks a single operation
When you write a custom command in your Rust code, you can define permissions for it following the same pattern. The identifier becomes part of your application's schema, and it appears in the IDE autocompletion for your capability files.
Scoped Permissions
Some permissions accept a scope — an object that further restricts what the command can do even after permission is granted. The most common example is filesystem access. Instead of granting blanket read permission, you scope it to specific directories:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Scoped filesystem access",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:read-files",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$RESOURCE/**" }
]
},
{
"identifier": "fs:write-files",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
The $APPDATA and $RESOURCE variables are path aliases that Tauri resolves at runtime. $APPDATA points to the platform-appropriate application data directory. $RESOURCE points to the bundled resources. The ** glob matches any number of subdirectories. This configuration means the frontend can read anything in the app data and resource directories, but it can only write to the app data directory — and it cannot touch anything outside those roots.
The scope check happens in the Rust command, not in the frontend. Even if a compromised script constructs a path like ../../etc/passwd, the command's canonicalization and prefix check reject it before any file I/O occurs.
Security Configuration
The app.security section of tauri.conf.json controls global security settings that affect the entire application. It is where you enable or disable features like the isolation pattern, asset protocol restrictions, and the global Tauri object.
The security Object in tauri.conf.json
Here is the full structure with all default values:
{
"app": {
"security": {
"assetProtocol": {
"enable": false,
"scope": []
},
"capabilities": [],
"dangerousDisableAssetCspModification": false,
"freezePrototype": false,
"pattern": {
"use": "brownfield"
}
}
}
}
The Isolation Pattern
Tauri v2 supports two IPC patterns: brownfield and isolation. The brownfield pattern is the default — the frontend communicates directly with the Rust backend through invoke and emit calls. It is the simplest to set up and works for most applications.
The isolation pattern inserts a sandboxed JavaScript layer between the frontend and the backend. Every IPC message passes through an isolation application — a small HTML and JavaScript bundle that runs in a sandboxed <iframe> with its own origin. The isolation application can inspect, validate, modify, or reject messages before they reach the Rust core.
This pattern exists for one reason: untrusted code in the frontend. If your application loads third-party plugins, user-authored scripts, or remote content that you do not fully control, that code runs in the same WebView as your trusted application code — and it can call invoke just like your code can. The isolation pattern means those calls go through a gatekeeper first.
When to Use the Isolation Pattern:
The isolation pattern adds complexity and a small performance cost from the extra message hop and encryption. It is not necessary for applications where all frontend code is your own. Use it when your app loads content you did not write: plugin systems, embedded third-party widgets, or remote pages from domains you do not fully trust.
To enable isolation, create an isolation application:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Isolation Secure Script</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
window.__TAURI_ISOLATION_HOOK__ = (payload) => {
// Inspect and optionally modify every IPC message
console.log("IPC message intercepted:", payload);
// Return the payload to allow it, or throw to block it
return payload;
};
Then configure Tauri to use it:
{
"build": {
"distDir": "../dist"
},
"app": {
"security": {
"pattern": {
"use": "isolation",
"options": {
"dir": "../dist-isolation"
}
}
}
}
}
The isolation application runs in a sandboxed <iframe> using the browser's SubtleCrypto API to encrypt messages. New encryption keys are generated every time the application starts, so a key compromised in one session does not affect the next.
Freeze Prototype
The freezePrototype setting, when set to true, calls Object.freeze() on the prototypes of built-in JavaScript objects like Object, Array, and Function. This prevents frontend code — including any malicious scripts that find their way into the WebView — from modifying the behavior of fundamental JavaScript operations.
A common attack vector involves overriding Array.prototype.push or Object.prototype.hasOwnProperty to intercept data flowing through the application. Freezing the prototypes makes these overrides impossible.
{
"app": {
"security": {
"freezePrototype": true
}
}
}
The cost is that legitimate polyfills and prototype extensions stop working. If your frontend depends on a library that modifies built-in prototypes, enable this only after verifying compatibility.
Asset Protocol Scope
The assetProtocol setting controls whether Tauri serves frontend assets through a custom protocol and which directories it can access. When enabled, assets are loaded through https://asset.localhost/ instead of the default origin. The scope array limits which directories the protocol can serve from:
{
"app": {
"security": {
"assetProtocol": {
"enable": true,
"scope": ["$RESOURCE/**", "$APPDATA/assets/**"]
}
}
}
}
This is useful when your application needs to load assets from the file system dynamically — for example, user-uploaded images or downloaded content — without exposing the entire file system through the protocol.
dangerousDisableAssetCspModification
By default, Tauri modifies the Content Security Policy of asset responses to ensure they work correctly with the custom protocol. Setting dangerousDisableAssetCspModification to true prevents this modification. The word "dangerous" in the name is not decoration — disabling this means you are fully responsible for ensuring CSP headers are correct on every asset response, and a misconfiguration can break the application or open security holes.
{
"app": {
"security": {
"dangerousDisableAssetCspModification": true
}
}
}
Leave this at false unless you have a specific reason to manage CSP headers yourself at the asset level.
Content Security Policy
Content Security Policy is a browser feature that tells the WebView which resources it is allowed to load, connect to, and execute. In a Tauri application, CSP is your first line of defense against cross-site scripting. If a malicious script somehow enters the WebView, a correctly configured CSP prevents it from phoning home, loading additional payloads, or executing inline code.
CSP Misconfiguration is a Real Threat:
A CSP that includes 'unsafe-eval' or broad connect-src directives allows injected scripts to execute arbitrary code and exfiltrate data. In a Tauri app, where the frontend has access to native APIs through the capabilities system, a weak CSP can turn an XSS vulnerability into a full system compromise.
Configuring CSP in tauri.conf.json
The CSP is set in the app.security.csp field. It is either a string (the entire policy as one value) or an object where each key is a directive and each value is a string or array of sources:
{
"app": {
"security": {
"csp": {
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self' 'unsafe-inline'",
"connect-src": "'self' ipc: http://ipc.localhost",
"img-src": "'self' asset: http://asset.localhost blob: data:",
"font-src": "'self' https://fonts.gstatic.com",
"object-src": "'none'",
"frame-ancestors": "'none'"
}
}
}
}
Each directive controls a specific resource type. default-src is the fallback for any type not explicitly listed. 'self' means only resources from the same origin as the application. The special values ipc: and asset: are Tauri-specific protocols for internal communication and asset loading.
What Each Directive Controls
The script-src directive determines where JavaScript can be loaded from. Setting it to 'self' blocks inline <script> tags and eval() calls — both common XSS vectors. If you need inline styles (which most CSS-in-JS libraries require), add 'unsafe-inline' to style-src — but never add it to script-src unless you have no alternative.
The connect-src directive controls which URLs the frontend can reach via fetch, XMLHttpRequest, and WebSocket connections. The ipc: source is mandatory for Tauri's internal IPC to function. Without it, invoke and emit calls fail silently.
The img-src directive is often the first place developers add remote sources — for loading user avatars, CDN-hosted images, or blob URLs from canvas operations. The data: scheme allows inline base64 images; blob: allows images created programmatically through the Blob API.
CSP is Not a Permission System:
CSP controls what the WebView loads and connects to at the browser level. It does not control which Tauri commands the frontend can call — that is the capabilities system. Both are necessary: CSP keeps untrusted code from executing in the first place; capabilities limit the damage if it does.
Tauri-Specific CSP Values
Tauri uses a few custom scheme values in CSP directives that are specific to its architecture:
ipc:— allows the frontend to communicate with the Rust backend through Tauri's IPC channel. This must be present inconnect-srcordefault-src.asset:— allows loading assets through Tauri's asset protocol. Required inimg-src,script-src, orstyle-srcif your app loads bundled assets through the protocol.http://ipc.localhost— the host used for IPC on Windows and Linux. Include it inconnect-srcalongsideipc:.http://asset.localhost— the host used for asset loading on some platforms.
Omitting ipc: from your CSP is one of the most common configuration errors. The symptom is that all invoke() calls fail, and the console shows CSP violation errors mentioning connect-src.
Example: A Production-Ready CSP
For an application that loads fonts from Google Fonts, images from a CDN, and connects to its own API server, a production CSP might look like this:
{
"app": {
"security": {
"csp": {
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self' 'unsafe-inline' https://fonts.googleapis.com",
"connect-src": "'self' ipc: http://ipc.localhost https://api.myapp.com",
"img-src": "'self' asset: http://asset.localhost blob: data: https://cdn.myapp.com",
"font-src": "'self' https://fonts.gstatic.com",
"object-src": "'none'",
"frame-ancestors": "'none'"
}
}
}
}
This policy allows the application to function normally — IPC calls, asset loading, API requests, CDN images, and Google Fonts all work. It blocks <object>, <embed>, and <frame> elements entirely, and it prevents the app from being embedded in an iframe on another page. Inline scripts and eval() are blocked, which means your frontend build tool must not generate code that relies on them.
CSP Validation:
You can validate your CSP by opening the developer tools in your Tauri application during development. CSP violations appear as errors in the console with the exact directive that blocked the request. Adjust the policy one directive at a time until only the connections you expect are allowed.
Common Configuration Mistakes
Several patterns appear repeatedly in projects moving from Tauri v1 to v2 or starting fresh.
Assuming v1 defaults carry over. Tauri v1 had an allowlist system where APIs were available unless explicitly blocked. Tauri v2 flipped this: everything is denied by default. If a command silently fails, the answer is almost always a missing permission in a capability file, not a bug in the Rust code.
Using "*" for windows in production. The wildcard is convenient during prototyping because every window automatically gets the permissions. But it also means that a dynamically created popup window — perhaps opened by a compromised dependency — inherits every permission. List windows explicitly.
Forgetting ipc: in the CSP. Without it, every invoke() call is blocked by the browser before it reaches Tauri's IPC layer. The error message in the console will mention connect-src, which is the clue.
Granting remote access too broadly. A capability with "urls": ["https://*"] and "permissions": ["fs:default"] means any HTTPS page loaded in your WebView can read and write files. If you need remote access, restrict it to specific domains and the smallest permission set possible.
Not using the schema reference. The $schema field in capability files enables autocompletion and catches typos at write time. Without it, a misspelled permission identifier becomes a runtime error that might not surface until a specific feature is used. Always include the schema.
Summary
The Tauri v2 security model is built on the principle that the frontend is untrusted — not because you wrote malicious code, but because the web ecosystem's supply chain makes it impossible to guarantee that every dependency will always be safe. Capabilities, permissions, and CSP are three layers that work together to contain the blast radius of a frontend compromise.
The capabilities system maps permissions to windows. Each capability file answers the question: "which windows can call which commands?" Plugin permissions extend this to third-party plugins, each shipping its own set of grantable operations with optional scopes. The security configuration in tauri.conf.json handles global settings like the isolation pattern and prototype freezing. Content Security Policy prevents malicious code from loading, connecting, or executing in the first place.
The most important operational takeaway is that every permission must be explicitly granted. When something does not work — a file will not read, a window title will not change — the first place to look is the capability file for that window, then the CSP, then the command's scope configuration.
If your application loads content you did not write, enable the isolation pattern and tighten the CSP further. If your application only runs your own bundled code, the brownfield pattern with explicit window labels and minimal permissions is the right balance of security and simplicity.
Capability Files
Understand Tauri v2 capability files and how they enforce the least-privilege security model by controlling which permissions each window receives
Plugin Permissions
Learn how to configure granular plugin permissions in Tauri v2 to control which commands each window can execute, using capability files and scoped access rules.
Security Configuration
Learn how to lock down your Tauri v2 app using the principle of least privilege, capabilities, permissions, and CSP to protect your users and system
Content Security Policy (CSP)
Learn how to configure a Content Security Policy in your Tauri v2 application to protect against cross-site scripting and control which resources the webview can load.