mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat: add fuzzing targets for untrusted input parsers (#835)
* 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 <[email protected]> * fix: improve fuzz targets to exercise real IronClaw code paths - fuzz_config_env: exercise SafetyLayer end-to-end (sanitize, validate, policy check) instead of generic TOML/JSON parsing - fuzz_tool_params: add validate_tool_schema coverage alongside validate_tool_params - Add "fuzz" to workspace exclude in root Cargo.toml - Update README descriptions to match actual target behavior [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: replace redundant detect() call with meaningful invariant assertion Replace the double sanitize()+detect() call with an assertion that critical severity warnings always trigger content modification. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: rewrite fuzz_config_env to exercise IronClaw safety code directly Replace SafetyLayer wrapper usage with direct Sanitizer, Validator, and LeakDetector instantiation and invocation. Adds meaningful consistency assertions (non-empty output, valid-means-no-errors, scan/clean agreement). Removes the config construction that was only exercising struct instantiation. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9d8817646d
commit
c148dd2b5b
@@ -14,6 +14,7 @@ exclude = [
|
|||||||
"tools-src/google-slides",
|
"tools-src/google-slides",
|
||||||
"tools-src/slack",
|
"tools-src/slack",
|
||||||
"tools-src/telegram",
|
"tools-src/telegram",
|
||||||
|
"fuzz",
|
||||||
]
|
]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 and schema JSON validation |
|
||||||
|
| `fuzz_config_env` | SafetyLayer end-to-end (sanitize, validate, policy check) |
|
||||||
|
|
||||||
|
## 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_<name>.rs` following the existing pattern
|
||||||
|
2. Add a `[[bin]]` entry in `fuzz/Cargo.toml`
|
||||||
|
3. Create `fuzz/corpus/fuzz_<name>/` for seed inputs
|
||||||
|
4. Exercise real IronClaw code paths, not just generic serde
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#![no_main]
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
use ironclaw::safety::{LeakDetector, Sanitizer, Validator};
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if let Ok(input) = std::str::from_utf8(data) {
|
||||||
|
// Exercise Sanitizer: detect and neutralize prompt injection attempts.
|
||||||
|
let sanitizer = Sanitizer::new();
|
||||||
|
let sanitized = sanitizer.sanitize(input);
|
||||||
|
// The sanitized content must never be empty when input is non-empty,
|
||||||
|
// because sanitization wraps/escapes rather than deleting.
|
||||||
|
if !input.is_empty() {
|
||||||
|
assert!(
|
||||||
|
!sanitized.content.is_empty(),
|
||||||
|
"sanitize() produced empty content for non-empty input"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If no modification occurred, content must equal input.
|
||||||
|
if !sanitized.was_modified {
|
||||||
|
assert_eq!(sanitized.content, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise Validator: input validation (length, encoding, patterns).
|
||||||
|
let validator = Validator::new();
|
||||||
|
let result = validator.validate(input);
|
||||||
|
// ValidationResult must always be well-formed: if valid, no errors.
|
||||||
|
if result.is_valid {
|
||||||
|
assert!(
|
||||||
|
result.errors.is_empty(),
|
||||||
|
"valid result should have no errors"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise LeakDetector: secret detection (API keys, tokens, etc.).
|
||||||
|
let detector = LeakDetector::new();
|
||||||
|
let scan = detector.scan(input);
|
||||||
|
// scan_and_clean must not panic and must return valid UTF-8.
|
||||||
|
let cleaned = detector.scan_and_clean(input);
|
||||||
|
if let Ok(ref clean_str) = cleaned {
|
||||||
|
// Cleaned output must never be longer than original + redaction markers.
|
||||||
|
// At minimum it should be valid UTF-8 (guaranteed by String type).
|
||||||
|
let _ = clean_str.len();
|
||||||
|
}
|
||||||
|
// If scan found no matches, scan_and_clean should return the input unchanged.
|
||||||
|
if scan.matches.is_empty() {
|
||||||
|
if let Ok(ref clean_str) = cleaned {
|
||||||
|
assert_eq!(
|
||||||
|
clean_str, input,
|
||||||
|
"scan_and_clean changed content despite no matches"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#![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());
|
||||||
|
}
|
||||||
|
// Verify invariant: critical severity triggers modification
|
||||||
|
let has_critical = result.warnings.iter().any(|w| {
|
||||||
|
w.severity == ironclaw::safety::Severity::Critical
|
||||||
|
});
|
||||||
|
if has_critical {
|
||||||
|
assert!(result.was_modified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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::<serde_json::Value>(s) {
|
||||||
|
let _ = validator.validate_tool_params(&value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#![no_main]
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use ironclaw::safety::Validator;
|
||||||
|
use ironclaw::tools::validate_tool_schema;
|
||||||
|
|
||||||
|
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::<serde_json::Value>(s) {
|
||||||
|
// Exercise Validator::validate_tool_params with arbitrary JSON
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise validate_tool_schema with arbitrary JSON as a schema
|
||||||
|
let _ = validate_tool_schema(&value, "fuzz");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user