One query per row
Lists all items, then loads each one again
$ appfuzz --human https://appfuzz.dev/demo/chatty.wasm
for it in items::all() {
if let Ok(full) = items::get(it.id) {
out += &format!("<li>{}</li>", esc(&full.owner));
}
}fail n_plus_1 calls to items#get grow with the length of the list items#all returns; the request goes over its limit at 100 rows
A search box
The query goes straight into the page
$ appfuzz --human https://appfuzz.dev/demo/reflect-query.wasm
let q = param(&req.query, "q").unwrap_or("");
html(format!("<p>Results for {q}</p>"))fail reflection the request field query.q reaches the HTML unescaped
The first account with no items
Works on the demo data, crashes on an empty list
$ appfuzz --human https://appfuzz.dev/demo/empty-panic.wasm
let all = items::all();
let first = &all[0];
html(format!("<p>First: {}</p>", esc(&first.name)))fail crash when items#all returns an empty list: index out of bounds
Names in other languages
Cuts names at 8 bytes, which breaks inside a character
$ appfuzz --human https://appfuzz.dev/demo/utf8-slice.wasm
for it in items::all() {
let short = if it.name.len() > 8 { &it.name[..8] } else { it.name.as_str() };
out += &format!("<li>{}</li>", esc(short));
}fail crash when items#all returns names with multi-byte characters: byte 8 is not a character boundary
And one that passes
One read, sorted, escaped, at most 20 rows
$ appfuzz --human https://appfuzz.dev/demo/clean.wasm
let mut all = items::all();
all.sort_by(|a, b| b.score.cmp(&a.score).then(a.id.cmp(&b.id)));
let q = param(&req.query, "q").unwrap_or("");
let mut out = format!("<h1>Items matching {}</h1><ul>", esc(q));
for it in all.iter().filter(|i| i.name.contains(q)).take(20) {
out += &format!("<li>{} ({})</li>", esc(&it.name), it.score);
}pass no findings
The full source of each app is in bench/apps on GitHub. To review your own app, run appfuzz review next to its .wasm.