From e41eb8ae33ad2fc3ddeab56aa4e5ce096a2309f5 Mon Sep 17 00:00:00 2001 From: "ilblackdragon@gmail.com" Date: Mon, 9 Mar 2026 23:05:58 -0700 Subject: [PATCH] feat: add fuzzing targets for untrusted input parsers Add cargo-fuzz infrastructure with 5 fuzz targets exercising security-critical code paths: - fuzz_safety_sanitizer: Aho-Corasick + regex injection detection - fuzz_safety_validator: Input validation (length, encoding, patterns) - fuzz_leak_detector: Secret leak scanning (API keys, tokens) - fuzz_tool_params: Tool parameter JSON validation - fuzz_config_env: TOML/JSON config parsing Each target exercises real IronClaw business logic with invariant assertions. Includes corpus directories and setup documentation. Co-Authored-By: Claude Opus 4.6 --- fuzz/Cargo.toml | 40 ++++++++++++++++++++ fuzz/README.md | 43 ++++++++++++++++++++++ fuzz/corpus/fuzz_config_env/.gitkeep | 0 fuzz/corpus/fuzz_leak_detector/.gitkeep | 0 fuzz/corpus/fuzz_safety_sanitizer/.gitkeep | 0 fuzz/corpus/fuzz_safety_validator/.gitkeep | 0 fuzz/corpus/fuzz_tool_params/.gitkeep | 0 fuzz/fuzz_targets/fuzz_config_env.rs | 12 ++++++ fuzz/fuzz_targets/fuzz_leak_detector.rs | 23 ++++++++++++ fuzz/fuzz_targets/fuzz_safety_sanitizer.rs | 20 ++++++++++ fuzz/fuzz_targets/fuzz_safety_validator.rs | 21 +++++++++++ fuzz/fuzz_targets/fuzz_tool_params.rs | 17 +++++++++ 12 files changed, 176 insertions(+) create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/README.md create mode 100644 fuzz/corpus/fuzz_config_env/.gitkeep create mode 100644 fuzz/corpus/fuzz_leak_detector/.gitkeep create mode 100644 fuzz/corpus/fuzz_safety_sanitizer/.gitkeep create mode 100644 fuzz/corpus/fuzz_safety_validator/.gitkeep create mode 100644 fuzz/corpus/fuzz_tool_params/.gitkeep create mode 100644 fuzz/fuzz_targets/fuzz_config_env.rs create mode 100644 fuzz/fuzz_targets/fuzz_leak_detector.rs create mode 100644 fuzz/fuzz_targets/fuzz_safety_sanitizer.rs create mode 100644 fuzz/fuzz_targets/fuzz_safety_validator.rs create mode 100644 fuzz/fuzz_targets/fuzz_tool_params.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..d6865a24 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "ironclaw-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +serde_json = "1" + +[dependencies.ironclaw] +path = ".." + +[[bin]] +name = "fuzz_safety_sanitizer" +path = "fuzz_targets/fuzz_safety_sanitizer.rs" +doc = false + +[[bin]] +name = "fuzz_safety_validator" +path = "fuzz_targets/fuzz_safety_validator.rs" +doc = false + +[[bin]] +name = "fuzz_leak_detector" +path = "fuzz_targets/fuzz_leak_detector.rs" +doc = false + +[[bin]] +name = "fuzz_tool_params" +path = "fuzz_targets/fuzz_tool_params.rs" +doc = false + +[[bin]] +name = "fuzz_config_env" +path = "fuzz_targets/fuzz_config_env.rs" +doc = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..13a31565 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,43 @@ +# IronClaw Fuzz Targets + +Fuzz testing for security-critical input parsing paths using [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) (libFuzzer). + +## Targets + +| Target | What it exercises | +|--------|-------------------| +| `fuzz_safety_sanitizer` | Prompt injection pattern detection (Aho-Corasick + regex) | +| `fuzz_safety_validator` | Input validation (length, encoding, forbidden patterns) | +| `fuzz_leak_detector` | Secret leak detection (API keys, tokens, credentials) | +| `fuzz_tool_params` | Tool parameter JSON validation pipeline | +| `fuzz_config_env` | Configuration parsing (TOML, JSON) | + +## Setup + +```bash +cargo install cargo-fuzz +rustup install nightly +``` + +## Running + +```bash +# Run a specific target (runs until stopped or crash found) +cargo +nightly fuzz run fuzz_safety_sanitizer + +# Run with a time limit (5 minutes) +cargo +nightly fuzz run fuzz_leak_detector -- -max_total_time=300 + +# Run all targets for 60 seconds each +for target in fuzz_safety_sanitizer fuzz_safety_validator fuzz_leak_detector fuzz_tool_params fuzz_config_env; do + echo "==> $target" + cargo +nightly fuzz run "$target" -- -max_total_time=60 +done +``` + +## Adding New Targets + +1. Create `fuzz/fuzz_targets/fuzz_.rs` following the existing pattern +2. Add a `[[bin]]` entry in `fuzz/Cargo.toml` +3. Create `fuzz/corpus/fuzz_/` for seed inputs +4. Exercise real IronClaw code paths, not just generic serde diff --git a/fuzz/corpus/fuzz_config_env/.gitkeep b/fuzz/corpus/fuzz_config_env/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/fuzz/corpus/fuzz_leak_detector/.gitkeep b/fuzz/corpus/fuzz_leak_detector/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/fuzz/corpus/fuzz_safety_sanitizer/.gitkeep b/fuzz/corpus/fuzz_safety_sanitizer/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/fuzz/corpus/fuzz_safety_validator/.gitkeep b/fuzz/corpus/fuzz_safety_validator/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/fuzz/corpus/fuzz_tool_params/.gitkeep b/fuzz/corpus/fuzz_tool_params/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/fuzz/fuzz_targets/fuzz_config_env.rs b/fuzz/fuzz_targets/fuzz_config_env.rs new file mode 100644 index 00000000..52c8943c --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_config_env.rs @@ -0,0 +1,12 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Exercise TOML config parsing with arbitrary input + let _ = toml::from_str::(s); + + // Exercise JSON config parsing + let _ = serde_json::from_str::(s); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_leak_detector.rs b/fuzz/fuzz_targets/fuzz_leak_detector.rs new file mode 100644 index 00000000..f1e6e09c --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_leak_detector.rs @@ -0,0 +1,23 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use ironclaw::safety::LeakDetector; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let detector = LeakDetector::new(); + + // Exercise scan path + let result = detector.scan(s); + // Invariant: if should_block, there must be matches + if result.should_block { + assert!(!result.matches.is_empty()); + } + // Invariant: match locations must be valid + for m in &result.matches { + assert!(m.location.end <= s.len()); + } + + // Exercise scan_and_clean path + let _ = detector.scan_and_clean(s); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs b/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs new file mode 100644 index 00000000..8c3c54a2 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_safety_sanitizer.rs @@ -0,0 +1,20 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use ironclaw::safety::Sanitizer; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let sanitizer = Sanitizer::new(); + + // Exercise the main sanitization path + let result = sanitizer.sanitize(s); + // Verify invariant: warnings should have valid ranges + for w in &result.warnings { + assert!(w.location.end <= s.len()); + } + + // Exercise detection-only path + let warnings = sanitizer.detect(s); + assert_eq!(warnings.len(), result.warnings.len()); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_safety_validator.rs b/fuzz/fuzz_targets/fuzz_safety_validator.rs new file mode 100644 index 00000000..065bc86d --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_safety_validator.rs @@ -0,0 +1,21 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use ironclaw::safety::Validator; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let validator = Validator::new(); + + // Exercise input validation + let result = validator.validate(s); + // Invariant: empty input is always invalid + if s.is_empty() { + assert!(!result.is_valid); + } + + // Exercise tool parameter validation with arbitrary JSON + if let Ok(value) = serde_json::from_str::(s) { + let _ = validator.validate_tool_params(&value); + } + } +}); diff --git a/fuzz/fuzz_targets/fuzz_tool_params.rs b/fuzz/fuzz_targets/fuzz_tool_params.rs new file mode 100644 index 00000000..201477b6 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tool_params.rs @@ -0,0 +1,17 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use ironclaw::safety::Validator; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Try parsing as JSON and validating as tool parameters + if let Ok(value) = serde_json::from_str::(s) { + let validator = Validator::new(); + let result = validator.validate_tool_params(&value); + // Invariant: result should always be well-formed + if !result.is_valid { + assert!(!result.errors.is_empty()); + } + } + } +});