Fuzzing That Finds Bugs, Not Just Crashes
Fuzzing has a reputation as "send garbage until something breaks." Done that way it finds nothing but rate limits. Done well, it's a structured search of the input space, guided by coverage or by a hypothesis, and it surfaces bugs no manual reviewer would reach. Here's how we actually run it across three surfaces.
Web API fuzzing — content discovery and parameter mining
Against a web target, "fuzzing" mostly means intelligent enumeration:
- Content discovery with curated, tech-specific wordlists — not
common.txtagainst everything. Fingerprint the stack first, then fuzz paths, extensions, and backup/temp patterns matched to it (.bak,~,.old,.swp, source-map files,/.git/). - Parameter mining. Endpoints honor parameters the docs and UI never mention —
debug,admin,test,internal,format=,callback=, feature flags. Fuzz parameter names against a stable endpoint and diff the responses; a changed status, size, or timing reveals a hidden lever. - Value fuzzing at known sinks. Once you've found a parameter that reaches a template, a query, or a fetch, fuzz its values with class-specific payload sets (SSTI markers, boundary numbers, encoding variants) rather than a generic blob.
The signal is always a differential — a response that differs from the baseline in status, length, timing, or reflected content. Automate the diffing; that's the whole skill.
Parser and file-format fuzzing — coverage-guided
For anything that parses complex input (media, documents, archives, protocol messages), reach for coverage-guided fuzzing:
- Seed corpus matters more than the fuzzer. Start from real, valid files that exercise many code paths; a good corpus of diverse valid inputs beats a clever mutator on a thin one.
- Instrument for coverage (AFL++/libFuzzer where you have the binary or can build a harness) so the fuzzer evolves inputs toward new code, not random noise. Coverage feedback is the difference between finding a bug in an hour and never finding one.
- Sanitizers turn silent corruption into signal — ASan/UBSan/MSan make memory bugs crash loudly and near the root cause, so a "crash" is a triageable bug, not a mystery.
- Structure-aware mutation. For formats with checksums or nested grammars, a naive bit-flip dies at the first integrity check. Teach the fuzzer the grammar (or fix up checksums post-mutation) so mutations reach deep parsing logic.
Protocol and state fuzzing
For stateful protocols, the bugs are in the sequence, not a single message:
- Model the state machine, then fuzz transitions — messages out of order, repeated, skipped, or malformed at a specific state. Many high-impact bugs are "server accepts message X before authentication."
- Differential fuzzing: send the same input to two implementations of the same protocol/format and hunt disagreements. A parser differential is the bug class behind request smuggling, SSRF filter bypass, and auth confusion.
Triage is the real work
A fuzzer produces crashes; you produce findings:
- Minimize every crashing input to the smallest bytes that reproduce it.
- De-duplicate by root cause (stack hash), not by input — a thousand crashes are often one bug.
- Assess exploitability honestly: a null-deref DoS and a controllable heap overflow are very different reports.
- Reproduce deterministically before writing anything up.
Discipline
Fuzzing is loud. Against live targets it looks exactly like an attack and can degrade service — so throttle hard, prefer local harnesses and staging over production, respect scope and rate limits, and coordinate a window for anything aggressive. The goal is bugs, not an outage with your name on it. A precise, minimized, root-caused crash is worth a thousand raw ones — optimize for that, not for volume.