How to Implement File System Access in Tauri Securely: A Complete Guide
To implement file system access in Tauri securely, enable the fs plugin, define strict path glob patterns in tauri.conf.json, and extend permissions at runtime only after explicit user consent using AppHandle::fs_scope().
Tauri isolates the frontend from the operating system by default, requiring explicit opt-in for any file operations. When you implement file system access in Tauri securely, you configure the fs plugin with a Scope that defines exactly which paths are permitted or forbidden. According to the tauri-apps/tauri source code, all access checks are performed in Rust before any filesystem operation executes, ensuring the frontend can only touch explicitly whitelisted paths.
Understanding the Security Architecture
Tauri's filesystem security relies on a multi-layered approach that combines static configuration with runtime enforcement. The system ensures that forbidden patterns always override allowed patterns, providing defense-in-depth protection.
Core Security Components
FsScopeconfiguration – The JSON configuration intauri.conf.jsonthat declares baselineallowedPathsandforbiddenPathsusing glob patterns.Scopestruct – Defined incrates/tauri/src/scope/fs.rs, this holds compiled glob patterns, resolves symlinks, and determines if a path is permitted.tauri-plugin-fs– Exposes JavaScript APIs likereadFileandwriteFilethat forward calls to Rust where scope verification occurs.AppHandle::fs_scope()– Located incrates/tauri/src/lib.rs, this accessor allows runtime extension of the whitelist after user interaction.
How Permission Checks Work
When the application starts, Tauri parses the fs block from tauri.conf.json into a tauri_utils::config::FsScope value. The Scope::new constructor (lines 75-88 of fs.rs) processes each path through push_pattern, building three glob variants: the literal path, an escaped version, and a canonicalized parent.
During runtime, the Scope::is_allowed method (lines 58-71 of fs.rs) performs the following checks:
- Resolves symlinks and canonicalizes the incoming path using
try_resolve_symlink_and_canonicalize. - Tests against
forbidden_patternsfirst; any match immediately denies access. - If not forbidden, tests against
allowed_patterns; only matches permit access.
This logic guarantees that explicit forbid entries always win, even if a path matches both lists.
Static Configuration vs. Runtime Permissions
Secure file system access requires configuring both baseline restrictions and dynamic extensions. The static configuration establishes a minimal attack surface at startup, while runtime permissions grant temporary access based on user actions.
Defining the Baseline Scope
Configure strict defaults in tauri.conf.json before the application launches:
{
"tauri": {
"security": { "csp": "default-src 'self'" },
"allowlist": { "fs": { "all": false } },
"fs": {
"allowedPaths": [
"$APPDATA/**",
"$HOME/Documents/**"
],
"forbiddenPaths": [
"$HOME/**/.ssh/**",
"$HOME/**/.gnupg/**"
]
}
}
}
Critical: Keep allowlist.fs.all set to false to prevent blanket access. Only the glob patterns listed in allowedPaths will be reachable at startup.
Extending Permissions at Runtime
When users select folders through native dialogs, grant access programmatically using the Scope API:
use tauri::Manager;
#[tauri::command]
async fn add_user_folder(app: tauri::AppHandle, folder: String) -> Result<(), String> {
let path = std::path::PathBuf::from(folder);
app.fs_scope()
.map_err(|e| e.to_string())?
.allow_directory(path, true)
.map_err(|e| e.to_string())
}
The second parameter (true in the example) enables recursive access to subdirectories. This approach ensures you implement file system access in Tauri securely by tying permissions to explicit user consent.
Step-by-Step Implementation
Follow these steps to enable secure file operations in your Tauri application.
1. Enable the FS Plugin
Add the required dependencies to your Cargo.toml:
[dependencies]
tauri = { version = "1", features = ["fs"] }
tauri-plugin-fs = "1"
2. Register the Plugin
Initialize the plugin in src/main.rs:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.setup(|app| {
// Example: pre-grant a specific directory during setup
let selected = std::path::PathBuf::from("C:/temp/user_data");
app.fs_scope()?.allow_directory(selected, true)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("failed to run Tauri app");
}
3. Configure Strict Baselines
Define minimal permissions in tauri.conf.json. Use environment variables like $APPDATA and $HOME for cross-platform compatibility:
{
"tauri": {
"fs": {
"allowedPaths": [
"$APPDATA/**"
],
"forbiddenPaths": [
"$HOME/**/.ssh/**"
]
}
}
}
4. Implement Frontend Operations
Use the TypeScript API to read and write files. Operations automatically fail if the path violates the current scope:
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
async function loadConfig() {
// Succeeds only if path matches allowed patterns
const content = await readTextFile('$APPDATA/config.json');
return content;
}
async function saveUserData(data: string) {
// Requires runtime permission grant first
await writeTextFile('$APPDATA/user/data.txt', data);
}
Monitoring Scope Changes
For auditing or UI updates, attach listeners to scope modifications using the Scope::listen method:
let id = app.fs_scope()?.listen(Box::new(|event| {
match event {
tauri::scope::Event::PathAllowed(p) =>
println!("Allowed: {}", p.display()),
tauri::scope::Event::PathForbidden(p) =>
println!("Forbidden: {}", p.display()),
}
}));
Store the returned id to call unlisten later if you need to remove the observer.
Key Source Files Reference
Understanding these implementation details helps debug permission issues:
| File | Purpose |
|---|---|
crates/tauri/src/scope/fs.rs |
Implements Scope::is_allowed, pattern matching, and the allow_*/forbid_* methods. |
crates/tauri/src/lib.rs |
Provides AppHandle::fs_scope() accessor at line 767. |
crates/tauri-utils/src/config.rs |
Defines FsScope configuration structure. |
examples/file-associations/src-tauri/src/main.rs |
Demonstrates real-world usage of runtime scope extension. |
Summary
- Never enable unrestricted access – Keep
allowlist.fs.alldisabled to prevent unauthorized file operations. - Define narrow static patterns – Use
tauri.conf.jsonto whitelist only essential directories like$APPDATA. - Extend scope only after user consent – Call
app.fs_scope()?.allow_directory()following dialog selection or explicit user action. - Leverage forbidden paths – Explicitly blacklist sensitive directories (
.ssh,.gnupg) to ensure they remain inaccessible even if allowed patterns are overly broad. - Audit scope changes – Use
Scope::listento track which paths become accessible during runtime.
Frequently Asked Questions
What's the difference between allowedPaths and forbiddenPaths in Tauri?
allowedPaths defines glob patterns that the frontend can access, while forbiddenPaths defines patterns that are explicitly blocked. According to the logic in crates/tauri/src/scope/fs.rs, the Scope::is_allowed method checks forbidden patterns first, meaning forbidden entries always override allowed ones even if a path matches both lists.
How do I grant file access after a user selects a folder in a dialog?
Use the AppHandle::fs_scope() method to extend permissions at runtime. After obtaining the path from a file dialog, call app.fs_scope()?.allow_directory(path, true) to grant recursive access. This pattern ensures you only implement file system access in Tauri securely when the user explicitly chooses the location.
Why does Tauri resolve symlinks before checking file permissions?
The Scope::is_allowed method calls try_resolve_symlink_and_canonicalize before pattern matching to prevent symlink attacks. Without this canonicalization, an attacker could create a symlink in an allowed directory pointing to a forbidden location (like /etc/passwd), bypassing security restrictions. The canonicalization ensures the actual resolved path is checked against your scope.
Can I restrict access to specific file extensions only?
The built-in Scope in fs.rs operates on path patterns rather than file extensions. To restrict by extension, either structure your allowedPaths globs to include extensions (e.g., $APPDATA/**/*.json) or implement additional validation in your Rust commands before calling filesystem operations. For granular extension control, combine scope patterns with explicit checks in your #[tauri::command] functions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →