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 crate crates/tauri-fuzz-cli that builds the binary cargo-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.

  1. Create the fuzz directory
mkdir -p mini-app/src-tauri/fuzz
  1. 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
  1. 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)
}
  1. Add build.rs and tauri.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 as mini-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.