Manual Steps to Fuzz your Tauri App
This is an extension to the [Quick Start guide][quick_start.md]. The different steps to fuzz a Tauri app are detailed here.
We will fuzz a very minimal Tauri application.
The repository features a minimal example called mini-app
.
This example app will be used to showcase how to setup fuzzing with tauri-fuzz
.
Prepare your Tauri App
Tauri app structure
mini-app
- ...
- src/
- src-tauri/
- src/
- lib.rs
- main.rs
- tauri_commands/
- file_access.rs
- read_foo_file
- ...
- Cargo.toml
The Tauri app backend must be compiled as a crate such that the Tauri commands are exposed to the fuzzer.
For example we want to fuzz the Tauri commands called read_foo_file
:
`mini-app/src-tauri/Cargo.toml`
[package]
name = "mini-app"
version = "0.0.0"
description = "A Tauri App"
# This section is automatic in Tauri v2
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]
`mini-app/src-tauri/lib.rs`
/// define the module
pub mod tauri_commands;
/// publicly re-export the Taur command `read_foo_file`
pub use tauri_commands::file_access::read_foo_file
`mini-app/src-tauri/src-tauri/tauri_commands/file_access.rs`
#[tauri::command]
/// Mark the function as public
pub fn read_foo_file() -> String {
let path = get_foo_path();
let mut content = String::new();
let mut file = File::open(path).unwrap();
file.read_to_string(&mut content).unwrap();
content
}
Create the application fuzz package
We will obtain this project structure:
Tauri app structure with fuzz directory
Project
- ...
- src-tauri
- src
- lib.rs
- main.rs
- tauri_commands
- file_access.rs
- read_foo_file
- ...
- fuzz
- build.rs
- Cargo.toml
- fuzz_targets/
- _template_.rs
- _template_full_.rs
- fuzzer_config.toml
- README.md
- tauri.conf.json
- Cargo.toml
With the CLI cargo-tauri-fuzz
[Note] This section requires the CLI utility
cargo-tauri-fuzz
The project contains the cratecrates/tauri-fuzz-cli
that builds the binarycargo-tauri-fuzz
. If any issue arises from using the CLI we recommend you follow the manual steps guide
Execute cargo-tauri-fuzz init
in mini-app/src-tauri
.
Setup the fuzz directory manually
You can copy-paste the example in the repo.
- Create the fuzz directory
mkdir -p mini-app/src-tauri/fuzz
- Add Cargo.toml in the fuzz directory
`mini-app/src-tauri/fuzz/Cargo.toml`
[package]
name = "{{ crate_name }}-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[workspace]
[build-dependencies]
tauri-build = "2.0"
[dependencies]
{{ crate_name }} = { path = ".." }
tauri-fuzz-policies = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git" }
tauri-fuzz = { git = "ssh://git@github.com/crabnebula-dev/tauri-fuzz.git", features = ["tauri"] }
tauri = { version = "2.0", features = ["test"]}
libafl = "0.13"
# Uncomment this block to add `fuzz_read_foo` as a fuzz target
# [[bin]]
# name = "fuzz_read_foo"
# path = "fuzz_targets/fuzz_read_foo.rs"
# doc = false
- Add fuzz_targets directory with templates
mkdir -p mini-app/src-tauri/fuzz/fuzz_targets
touch mini-app/src-tauri/fuzz_targets/_template_.rs
touch mini-app/src-tauri/fuzz_targets/_template_full_.rs
`mini-app/src-tauri/fuzz/fuzz_targets/_template_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// This is a template to create a fuzz target
//
// Steps:
// 1. Copy this file and rename it
// 2. Change the target details below
// 3. Add the new fuzz target in [[bin]] table in Cargo.toml of your project
//
// Note: you may need to implement [FromRandomBytes] for your command argument types.
tauri_fuzz::fuzz_tauri_command! {
// Name of the tauri command you want to fuzz
command: "read_foo_file",
// Pointer to the tauri command you want to fuzz
path: {{crate_name_underscored}}::file_access::read_foo_file,
// Parameters names and types to the tauri command
parameters: {
name: String,
},
// Policy chosen for the fuzzing
// Here the policy will not allow any access to the filesystem
policy: tauri_fuzz_policies::filesystem::no_file_access(),
}
`mini-app/src-tauri/fuzz/fuzz_targets/_template_full_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
use libafl::inputs::{BytesInput, HasMutatorBytes};
use libafl::prelude::ExitKind;
use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};
use tauri::webview::InvokeRequest;
use tauri::WebviewWindow;
/// This is a template to create a fuzz target
///
/// Steps:
/// 1. Copy this file and rename it
/// 2. Change `COMMAND_NAME` const value on line 25
/// 3. Change the path to your command in `tauri::generate_handler` on line 44
/// 4. Modify `create_request` to create arguments for your command on line 63
/// 5. Finally add the new fuzz target in [[bin]] table in Cargo.toml of your project
///
/// Note: you may need to implement [FromRandomBytes] for your command argument types.
///
use tauri_fuzz::tauri::{
create_invoke_request, invoke_command_minimal, CommandArgs, FromRandomBytes,
};
use tauri_fuzz::SimpleFuzzerConfig;
const COMMAND_NAME: &str = "read_foo_file";
fn main() {
let fuzz_dir = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
let fuzz_config_file = fuzz_dir.join("fuzzer_config.toml");
let options = SimpleFuzzerConfig::from_toml(fuzz_config_file, COMMAND_NAME, fuzz_dir).into();
tauri_fuzz::fuzz_main(
harness,
&options,
harness as *const () as usize,
tauri_fuzz_policies::filesystem::no_file_access(),
false,
);
}
// Setup the Tauri application mockruntime and an associated "main" webview
fn setup_mock() -> WebviewWindow<MockRuntime> {
let app = mock_builder()
.invoke_handler(
tauri::generate_handler![{{crate_name_underscored}}::file_access::read_foo_file],
)
.build(mock_context(noop_assets()))
.expect("Failed to init Tauri app");
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
.build()
.unwrap();
webview
}
// Harness function that will be repeated extensively by the fuzzer with semi-random bytes
// inputs
fn harness(input: &BytesInput) -> ExitKind {
let webview = setup_mock();
let _ = invoke_command_minimal(webview, create_request(input.bytes()));
ExitKind::Ok
}
// Helper code to create an `InvokeRequest` to send to the Tauri app backend
fn create_request(bytes: &[u8]) -> InvokeRequest {
let mut params = CommandArgs::new();
let param = String::from_random_bytes(&bytes).unwrap();
params.insert("name", param);
create_invoke_request(None, COMMAND_NAME, params)
}
- Add
build.rs
andtauri.conf.json
`mini-app/src-tauri/fuzz/build.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
/// Important: keep it here.
/// Currently this is necessary for Windows
fn main() {
tauri_build::build()
}
`mini-app/src-tauri/fuzz/tauri.conf.json`
{
"productName": "{{ crate_name }}-fuzz",
"version": "0.0.0",
"identifier": "dev.crabnebula.{{ crate_name }}-fuzz",
"bundle": {
"active": true,
"targets": "all",
"icon": ["../icons/icon.ico"]
}
}
Writing a Fuzz Target
We will finally create our fuzz target.
We fuzz the Tauri commands read_foo_file
which tries to read the file foo.txt
.
The fuzz policy that we will choose is tauri-fuzz-policies::file_policy::no_file_access()
that do not allow access to the filesystem.
There are two ways to write the fuzz target:
- with a Rust macro
fuzz_tauri_command
- manually by filling the template
Fill the template with macro
- Copy
mini-app/src-tauri/fuzz/fuzz_targets/_template_.rs
asmini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs
- Fill
mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs
with relevant information
`mini-app/src-tauri/fuzz/fuzz_targets/fuzz_read_foo.rs`
Here we will fuzz the Tauri command read_foo
against a policy that does not allow any file access.
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// This is a template to create a fuzz target
//
// Steps:
// 1. Copy this file and rename it
// 2. Change the target details below
// 3. Add the new fuzz target in [[bin]] table in Cargo.toml of your project
//
// Note: you may need to implement [FromRandomBytes] for your command argument types.
tauri_fuzz::fuzz_tauri_command! {
// Name of the tauri command you want to fuzz
command: "read_foo_file",
// Pointer to the tauri command you want to fuzz
path: {{crate_name_underscored}}::file_access::read_foo_file,
// Parameters names and types to the tauri command
parameters: {
name: String,
},
// Policy chosen for the fuzzing
// Here the policy will not allow any access to the filesystem
policy: tauri_fuzz_policies::filesystem::no_file_access(),
}
[Disclaimer] Our macro is not stable yet it may not work for complex cases. For more control over the fuzzing we suggest that you write the fuzz target manually by following the next section.
Fill the template with no macro
We are going to copy and modify the template file provided by
`crates/cli/template/fuzz_targets/_template_full_.rs`
// Copyright 2023-2024 CrabNebula Ltd., Alexandre Dang
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
use libafl::inputs::{BytesInput, HasMutatorBytes};
use libafl::prelude::ExitKind;
use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};
use tauri::webview::InvokeRequest;
use tauri::WebviewWindow;
/// This is a template to create a fuzz target
///
/// Steps:
/// 1. Copy this file and rename it
/// 2. Change `COMMAND_NAME` const value on line 25
/// 3. Change the path to your command in `tauri::generate_handler` on line 44
/// 4. Modify `create_request` to create arguments for your command on line 63
/// 5. Finally add the new fuzz target in [[bin]] table in Cargo.toml of your project
///
/// Note: you may need to implement [FromRandomBytes] for your command argument types.
///
use tauri_fuzz::tauri::{
create_invoke_request, invoke_command_minimal, CommandArgs, FromRandomBytes,
};
use tauri_fuzz::SimpleFuzzerConfig;
const COMMAND_NAME: &str = "read_foo_file";
fn main() {
let fuzz_dir = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
let fuzz_config_file = fuzz_dir.join("fuzzer_config.toml");
let options = SimpleFuzzerConfig::from_toml(fuzz_config_file, COMMAND_NAME, fuzz_dir).into();
tauri_fuzz::fuzz_main(
harness,
&options,
harness as *const () as usize,
tauri_fuzz_policies::filesystem::no_file_access(),
false,
);
}
// Setup the Tauri application mockruntime and an associated "main" webview
fn setup_mock() -> WebviewWindow<MockRuntime> {
let app = mock_builder()
.invoke_handler(
tauri::generate_handler![{{crate_name_underscored}}::file_access::read_foo_file],
)
.build(mock_context(noop_assets()))
.expect("Failed to init Tauri app");
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
.build()
.unwrap();
webview
}
// Harness function that will be repeated extensively by the fuzzer with semi-random bytes
// inputs
fn harness(input: &BytesInput) -> ExitKind {
let webview = setup_mock();
let _ = invoke_command_minimal(webview, create_request(input.bytes()));
ExitKind::Ok
}
// Helper code to create an `InvokeRequest` to send to the Tauri app backend
fn create_request(bytes: &[u8]) -> InvokeRequest {
let mut params = CommandArgs::new();
let param = String::from_random_bytes(&bytes).unwrap();
params.insert("name", param);
create_invoke_request(None, COMMAND_NAME, params)
}
Next steps:
- Fill COMMAND_NAME with
read_foo_file
const COMMAND_NAME: &str = "read_foo_file";
- in the harness, generate the Tauri app with the handle
mini-app::tauri_commands::file_access::read_foo_file
.invoke_handler(tauri::generate_handler![mini_app::tauri_commands::file_access::read_foo_file])
- fill the
create_payload
to invoke your Tauri command with the right parameters
fn create_payload(_bytes: &[u8]) -> InvokePayload {
let args = CommandArgs::new();
create_invoke_payload(None, COMMAND_NAME, args)
}
- specify the policy you want to apply
tauri-fuzz-policies::file_policy::no_file_access()
Start Fuzzing
Start fuzzing by executing one of these commands.
From mini-app/src-tauri/
directory:
cargo-tauri-fuzz fuzz fuzz_read_foo
Or from mini-app/src-tauri/fuzz/
directory:
cargo r --bin fuzz_read_foo
Validate Fuzzing Results
You should see this result repeatedly on your terminal:
Fuzzing results
[ERROR policies::engine] Policy was broken at function [open].
Description: Access to [open] denied
Rule: Rule::OnEntry
Context: Function entry with parameters: [140736965046336, 524288]
The application panicked (crashed).
Message: Intercepting call to [open].
Policy was broken at function [open].
Description: Access to [open] denied
Rule: Rule::OnEntry
Context: Function entry with parameters: [140736965046336, 524288]
This is the expected result. The Tauri command we fuzz, read_foo_file
, tries to read foo.txt
but got intercepted since we are fuzzing with a policy that does not allow any access to the filesystem.
More precisely the message specifies Policy was broken at function [open]
.
Indeed read_foo_file
tried to use the libc function open
that is used to access files and got intercepted.
The inputs used by the fuzzer which provokes a policy breach are stored in mini-app/src-tauri/fuzz/fuzz_solutions
.
Those inputs can be then investigated to understand why the policy breach happened.