Skip to content

Commit ae579e3

Browse files
authored
feat(fuzz): add template_fuzz target (#1147)
## Summary - Add fuzz target for the custom Mustache/Handlebars template engine in `builtins/template.rs` - Tests template rendering with JSON data input, `--strict` mode, and `-e` HTML escaping - Guards against deeply nested `{{#...}}` blocks Closes #1098 ## Test plan - [x] `cargo check` in fuzz crate passes - [x] `cargo fmt --check` clean - [x] `cargo clippy` clean
1 parent 80891dd commit ae579e3

File tree

2 files changed

+89
-0
lines changed

2 files changed

+89
-0
lines changed

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,10 @@ path = "fuzz_targets/yaml_fuzz.rs"
101101
test = false
102102
doc = false
103103
bench = false
104+
105+
[[bin]]
106+
name = "template_fuzz"
107+
path = "fuzz_targets/template_fuzz.rs"
108+
test = false
109+
doc = false
110+
bench = false
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Fuzz target for the template builtin
2+
//!
3+
//! Tests the custom Mustache/Handlebars template engine to find:
4+
//! - Panics on mismatched delimiters or deeply nested sections
5+
//! - Stack overflow from recursive block expansion
6+
//! - Edge cases in variable substitution and control flow blocks
7+
//! - Memory exhaustion from pathological template patterns
8+
//!
9+
//! Run with: cargo +nightly fuzz run template_fuzz -- -max_total_time=300
10+
11+
#![no_main]
12+
13+
use libfuzzer_sys::fuzz_target;
14+
15+
fuzz_target!(|data: &[u8]| {
16+
// Only process valid UTF-8
17+
if let Ok(input) = std::str::from_utf8(data) {
18+
// Limit input size to prevent OOM
19+
if input.len() > 1024 {
20+
return;
21+
}
22+
23+
// Split input into template (first line) and JSON data (rest)
24+
let (template, json_data) = match input.find('\n') {
25+
Some(pos) => (&input[..pos], &input[pos + 1..]),
26+
None => (input, "{\"name\":\"world\",\"items\":[1,2,3]}" as &str),
27+
};
28+
29+
// Skip empty templates
30+
if template.trim().is_empty() {
31+
return;
32+
}
33+
34+
// Reject deeply nested template blocks
35+
let depth = template.matches("{{#").count();
36+
if depth > 10 {
37+
return;
38+
}
39+
40+
let rt = tokio::runtime::Builder::new_current_thread()
41+
.enable_all()
42+
.build()
43+
.unwrap();
44+
45+
rt.block_on(async {
46+
let mut bash = bashkit::Bash::builder()
47+
.limits(
48+
bashkit::ExecutionLimits::new()
49+
.max_commands(50)
50+
.max_subst_depth(3)
51+
.max_stdout_bytes(4096)
52+
.max_stderr_bytes(4096)
53+
.timeout(std::time::Duration::from_millis(200)),
54+
)
55+
.build();
56+
57+
// Test 1: render template with JSON data via stdin
58+
let script = format!(
59+
"echo '{}' | template -d /dev/stdin '{}' 2>/dev/null; true",
60+
json_data.replace('\'', "'\\''"),
61+
template.replace('\'', "'\\''"),
62+
);
63+
let _ = bash.exec(&script).await;
64+
65+
// Test 2: render template with --strict mode
66+
let script2 = format!(
67+
"echo '{}' | template --strict -d /dev/stdin '{}' 2>/dev/null; true",
68+
json_data.replace('\'', "'\\''"),
69+
template.replace('\'', "'\\''"),
70+
);
71+
let _ = bash.exec(&script2).await;
72+
73+
// Test 3: render template with -e (HTML escape) flag
74+
let script3 = format!(
75+
"echo '{}' | template -e -d /dev/stdin '{}' 2>/dev/null; true",
76+
json_data.replace('\'', "'\\''"),
77+
template.replace('\'', "'\\''"),
78+
);
79+
let _ = bash.exec(&script3).await;
80+
});
81+
}
82+
});

0 commit comments

Comments
 (0)