Allowlisting Tauri Commands to Keep Frontend IPC Small and Safe
Treat Tauri #[command] handlers as a public API. Use an explicit invoke_handler allowlist, validate all arguments in Rust, and keep handlers async to avoid freezing the webview.
05 Jul 2025, 01:17 UTC

You ship a Tauri app and discover the frontend can reach any Rust function you exposed. The useful takeaway is to treat #[command] handlers as a public API: explicitly allowlist what JavaScript may call, validate every argument in Rust, and never let a handler block the webview.
The IPC surface you actually ship
Tauri routes calls from the webview to Rust through an OS webview IPC bridge. The frontend uses the JavaScript invoke API, and Rust exposes functions with the #[command] macro. The bridge itself is thin. It deserializes JSON from JavaScript and forwards it to a handler.
That means two things matter for security and stability. First, only commands registered in the invoke_handler allowlist are reachable. Functions not registered are unreachable from JavaScript, which reduces attack surface. Second, Tauri does not sanitize or validate JSON arguments automatically. Validation must happen inside Rust before any side effect.
Allowlist as a perimeter
Registration is explicit. In the Rust entry point you build the app with a list of commands:
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![read_file, write_file])
.run(tauri::generate_context!())
Only read_file and write_file can be invoked from the frontend. Renaming a handler or adding a new one without updating this list breaks the call from JavaScript, because the command name and signature are part of the public interface.
On the frontend a call looks like:
import { invoke } from '@tauri-apps/api/core'
const text = await invoke('read_file', { path: '/tmp/note.txt' })
Try invoking an unregistered name. The expected behavior is rejection by the runtime, not a silent no-op. This is a practical way to check that the allowlist is working.
Validate in Rust, keep the UI responsive
Do not trust data from the frontend even if the UI restricts input. A concrete, minimal pattern for a file read command:
use tauri::command;
#[command]
async fn read_file(path: String) -> Result {
if path.contains(\\"..\\") || path.starts_with('/') == false {
return Err(\\"invalid path\\".into());
}
tokio::fs::read_to_string(&path).await.map_err(|e| e.to_string())
}
Validation of type, range and length happens before I/O. Errors returned from commands are serialized to the frontend, so map errors to serializable types or plain strings to avoid opaque failures.
Command handlers share Tauri's async runtime. Long-running or blocking work should be moved off the handler thread. Use async I/O or spawn a blocking task. A deliberately blocking handler will freeze the webview and UI because the shared runtime is blocked.
Trade-off and limitations
An explicit allowlist is safer but adds maintenance cost. Every new capability requires a code change in Rust and a matching frontend call. The default webview backend varies by OS: WebView2 on Windows, WebKit on macOS, WebKitGTK on Linux. Invoke behavior is generally consistent, but edge cases around serialization and threading can differ, so verify on each target platform before shipping.
Check Cargo.toml for the installed Tauri version and review that version's command and allowlist documentation before adopting the pattern. Build and run on Windows, macOS, and Linux to confirm the command works consistently across backends.
Actionable check
- Register one #[command], call it from the frontend with invoke, then attempt an unregistered command name to confirm rejection.
- Add argument validation for type, length and allowed values inside every handler.
- Keep handlers async and non-blocking; move CPU work to spawned threads.
- Test the same build on each target webview backend.
When IPC is treated as a small, reviewed API surface with server-side validation and async discipline, the frontend stays fast and the Rust boundary stays predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.