Fuzz Testing
Discover vulnerabilities and crashes by feeding programs random, malformed, or unexpected inputs at scale. Covers coverage-guided fuzzing, grammar-based fuzzing, differential fuzzing, integration with CI/CD, and the patterns that find the bugs traditional testing misses.
Fuzz testing throws random, malformed, and unexpected inputs at your code to find crashes, memory leaks, and security vulnerabilities that human testers would never think to try. Fuzzing found critical vulnerabilities in Heartbleed (OpenSSL), Shellshock (Bash), and thousands of bugs in Chrome, Firefox, and the Linux kernel.
Why Fuzz
Traditional testing: Tests what you expect
test("valid email", () => validate("user@example.com")) ✓
test("empty string", () => validate("")) ✓
test("no @", () => validate("invalid")) ✓
Fuzz testing: Tests what you DON'T expect
Input: "\x00\xff\xfe" → CRASH (null byte handling)
Input: "a@" + "b" * 10000 → OOM (unbounded allocation)
Input: "user@exam\nple.com" → Header injection
Input: "user@[IPv6:::]" → Parser confusion
You cannot enumerate all edge cases.
Fuzzers explore inputs you would never write by hand.
Coverage-Guided Fuzzing
# Example: AFL (American Fuzzy Lop) workflow
# 1. Instrument your code for coverage tracking
# AFL modifies binary to report which code paths are hit
# 2. Provide seed inputs (valid examples)
# seeds/valid_json.txt: {"name": "test"}
# seeds/valid_xml.txt: <root><name>test</name></root>
# 3. Run the fuzzer
# afl-fuzz -i seeds/ -o findings/ -- ./my_parser @@
# AFL's algorithm:
# a. Take a seed input
# b. Mutate it (bit flip, byte insert, arithmetic)
# c. Run the program with mutated input
# d. If new code path covered → save as new seed
# e. If crash → save in findings/crashes/
# f. Repeat billions of times
# Python fuzzing with Atheris (Google)
import atheris
import sys
import json
def TestOneInput(data):
"""Called by fuzzer with random bytes."""
try:
fdp = atheris.FuzzedDataProvider(data)
json_str = fdp.ConsumeUnicode(1024)
# Function under test
parsed = json.loads(json_str)
# Differential: re-serialize and compare
reserialized = json.dumps(parsed)
reparsed = json.loads(reserialized)
assert parsed == reparsed, "Round-trip failure"
except (json.JSONDecodeError, UnicodeDecodeError):
pass # Expected failures, not bugs
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
Grammar-Based Fuzzing
# Generate syntactically valid but semantically interesting inputs
from hypothesis import given, strategies as st
# SQL injection fuzzing
sql_payloads = st.one_of(
st.just("' OR 1=1 --"),
st.just("'; DROP TABLE users; --"),
st.just("' UNION SELECT * FROM passwords --"),
st.text(
alphabet=st.characters(
whitelist_categories=("L", "N"),
whitelist_characters="'\"\\;-/* "
),
min_size=1,
max_size=1000
),
)
@given(user_input=sql_payloads)
def test_search_endpoint_safe_from_sql_injection(user_input):
response = client.get(f"/api/search?q={user_input}")
# Should never return server error (SQL syntax error = injection possible)
assert response.status_code != 500
assert "SQL" not in response.text.upper()
CI/CD Integration
# GitHub Actions: Run fuzzing on every PR
name: Fuzz Testing
on: [pull_request]
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run fuzzer (5 minute budget)
run: |
python -m atheris \
--fuzz_target=tests/fuzz_parser.py \
--max_total_time=300 \
--corpus_dir=corpus/ \
--artifact_prefix=crashes/
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: crashes/
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Fuzzing only in pre-release | Miss bugs during development | Continuous fuzzing in CI |
| No seed corpus | Fuzzer starts from scratch each run | Maintain corpus of interesting inputs |
| Catching all exceptions | Hide real bugs | Only catch expected exceptions |
| No crash triage | Same bug reported thousands of times | Deduplicate by stack trace |
| Fuzzing without sanitizers | Miss memory bugs | ASan, MSan, UBSan with fuzzer |
Fuzzing finds the bugs that code reviews, unit tests, and integration tests all miss. It is the safety net that catches what you never thought to test.