| name | fuzz-dynamic-security-test |
| description | Sets up dynamic security testing — coverage-guided fuzzing of parsers and input handlers (libFuzzer/cargo-fuzz/AFL++/go test -fuzz/atheris) and DAST scanning of a running app (OWASP ZAP/nuclei) — wired into CI with seed corpora, crash minimization, baseline suppression, and regression-corpus commits. |
| when_to_use | Hardening code that parses untrusted input, or a running web app, with active runtime testing that drives real inputs to provoke crashes/vulns. Distinct from write-tests (functional-correctness tests), security-review (static code audit), remediate-web-vulnerabilities (fixing a known vuln), and load-stress-test (performance under load). |
When to Use
Reach for this skill when you want to actively drive inputs at code or a running app to provoke crashes/vulns, not reason about them statically:
- "Fuzz this parser / deserializer / protocol decoder / image or PDF loader for crashes"
- "Set up cargo-fuzz / libFuzzer /
go test -fuzz / atheris / AFL++ with a seed corpus and run it in CI"
- "An input crashes / hangs / OOMs — minimize it and add a regression test"
- "Run OWASP ZAP / nuclei against staging, authenticated, and triage the findings"
- "Wire short fuzz on PR + long nightly fuzz, and DAST on every staging deploy"
NOT this skill:
- Functional correctness / example-based unit tests → write-tests
- Reading code by eye for injection/authz/secret bugs (no execution) → security-review
- Fixing a specific known SQLi/XSS/SSRF you already found → remediate-web-vulnerabilities
- Measuring latency/throughput/breaking point under concurrency → load-stress-test
- Reviewing an authorization design rather than testing it at runtime → design-authorization-model
If a finding is confirmed, hand the fix to remediate-web-vulnerabilities; this skill finds and reproduces, it does not patch app logic.
Steps
-
Pick the right tool for the target — do not write a fuzzer by hand. Coverage-guided engines mutate toward new code paths; random byte-spray finds nothing. Match the language:
| Target | Engine | Harness entry | Sanitizers |
|---|
| C/C++ | libFuzzer (clang -fsanitize=fuzzer) | LLVMFuzzerTestOneInput(const uint8_t*, size_t) | ASan + UBSan (+ MSan separately) |
| Rust | cargo-fuzz (libFuzzer under the hood) | fuzz_target!(|data: &[u8]| { ... }) | ASan on by default |
| Go | native go test -fuzz | func FuzzX(f *testing.F) + f.Fuzz(...) | race + built-in checks |
| Python | atheris (libFuzzer bindings) | atheris.Setup + TestOneInput(data) | native-ext ASan optional |
| JS/TS | Jazzer.js | module.exports.fuzz = (data) => {...} | n/a (catches throws) |
| Out-of-process C binary | AFL++ (afl-fuzz -i in -o out) | feed stdin/file | persistent mode + cmplog |
Default to the in-process libFuzzer-family engine for the language; reach for AFL++ only when you can't instrument the target (closed binary, weird build).
-
Fuzz the smallest deterministic boundary, structure-aware. Target one pure bytes → parsed value function — the deserializer, the codec/protocol decode, the template/expression parser — not the whole HTTP handler. Make it deterministic (no clock/network/RNG/global state). For structured formats, decode the byte buffer into typed inputs with arbitrary (Rust) / FuzzedDataProvider (C++/atheris) so mutations stay valid-ish and reach deep logic instead of dying at the length check. Rust example:
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::Arbitrary;
{ name: , depth: , body: <> }
fuzz_target!(|inp: Input| {
= my_parser::(&inp.name, inp.depth, &inp.body);
});
Common Errors
unwrap()/expect() in the harness on the parser's own error path. Every malformed input then "crashes" — pure noise. A returned Err is correct behavior; only a panic/abort/sanitizer-trip in the code under test is a finding.
- No seed corpus and no dictionary. The fuzzer burns the whole budget rediscovering the file magic/header and never reaches real logic. Seed with valid samples; add a token
.dict.
- Non-deterministic harness. Reading the clock, network, RNG, or mutating global state makes crashes non-reproducible and corrupts coverage feedback. The harness must be a pure function of
data.
- Committing the raw crashing input, not the minimized one. A 4 MB repro hides the root cause and bloats the corpus. Always
tmin/-minimize_crash first.
- Fuzzing the whole HTTP handler instead of the parser. Network/auth/DB setup dwarfs the parse step, so mutations rarely reach it — throughput collapses to a few execs/sec. Target the pure decode boundary in-process.
- No
-rss_limit_mb/-timeout/-max_len. OOMs and infinite loops get OS-killed and look like a hung job instead of a reported memory/hang bug. Set explicit limits.
- Sanitizers off (release build). Use-after-free, OOB read, and integer-UB pass silently without ASan/UBSan — you only catch hard segfaults. Build the fuzz target with sanitizers on.
- Running ZAP/nuclei against production. Active scans send malicious payloads, mutate data, and can take the service down. Always a disposable staging instance with test data.
- Unauthenticated DAST scan. Misses every logged-in route — the high-value surface. Configure auth (context/script/session token) and verify the scanner is actually inside a session.
- Muting a whole scanner rule to clear noise. Hides future real hits of that class. Suppress the specific accepted finding ID in a baseline file so only new findings fail the gate.
- Unbounded mutation fuzz on the PR job. Blocks every merge for an hour or times out. PR replays the corpus; the long mutation run goes nightly.
Verify
- Engine actually mutates and gains coverage: a short run shows rising
cov:/ft: and exec/s counters (libFuzzer) — not flat. Flat coverage means the harness rejects inputs at the door (wrong shape, missing arbitrary/FuzzedDataProvider).
- Planted-bug catch: add a deliberate
assert!/OOB/panic! on a specific byte pattern (or use a target with a known-CVE-style flaw), run the fuzzer, and confirm it finds and minimizes the input within minutes. A fuzzer that can't catch a planted bug catches nothing.
- Every crash yields a committed seed: for each crash found, the minimized input lives in the corpus/
testdata and is tracked in git. Re-running the harness over the corpus reproduces the crash deterministically.
- Regression gate works: with the seed committed but the bug unfixed, the PR corpus-replay job fails; after the fix it passes — proving the seed actually guards the regression.
- DAST reproduced + authenticated: at least one scanner finding is independently re-sent (curl/HTTP client) and reproduces; the scan log shows it traversed authenticated routes (logged-in paths visited), not just the login page.
- Baseline suppression is scoped: a previously-accepted finding is silenced by its specific ID, while a freshly introduced vuln of a different class still fails the gate (suppression didn't blanket the rule).
- CI tiers honored: PR fuzz finishes under its time cap (corpus replay only); nightly runs the bounded mutation budget and uploads any new crash as an artifact + files it.
Done = the engine provably mutates toward new coverage, a planted bug or known-CVE pattern is caught and minimized, every crash is committed as a regression seed that fails-then-passes across the fix, and DAST runs authenticated against staging with scoped baseline suppression and a two-tier CI wiring.