Why Fuzz Testing Matters - The Hidden Bug Hunter
Imagine you've built a perfect door lock that works flawlessly with every key you've ever tested. You've tried it with hundreds of different keys, and it always works perfectly. But one day, someone comes along with a bent paperclip, a hairpin, and a piece of bubble gum—and suddenly your "perfect" lock opens. That's the difference between unit testing and fuzz testing.
Real-World Disasters Found by Fuzzing:
- CVE-2021-33195: Go's
net.ParseIPvulnerability that caused buffer overflows with specially crafted IPv6 addresses. Found by fuzzing, affected millions of applications. - Heartbleed: The OpenSSL vulnerability that could have been discovered by fuzzing TLS handshake implementations. Cost: $500 million in global damages.
- Cloudflare Outage: A regex DoS vulnerability took down major websites. Could have been prevented with proper fuzz testing.
The Hard Truth: Your code has bugs you don't know about. Unit tests verify your code works with inputs you expect. Fuzz tests discover what happens with inputs you never imagined.
Learning Objectives
By the end of this article, you will be able to:
- Design effective fuzz tests that find bugs before they reach production
- Write fuzzable code with clear invariants and properties
- Use Go's native fuzzing to find security vulnerabilities and edge cases
- Implement coverage-guided fuzzing for maximum bug discovery
- Integrate fuzzing into CI/CD for continuous security testing
Core Concepts: Beyond Traditional Testing
Fuzz testing explores the vast space of unexpected inputs to find:
- Panics and crashes that cause service outages
- Security vulnerabilities like buffer overflows and injection attacks
- Logic errors that violate fundamental assumptions
- Performance issues through pathological inputs
Fuzzing vs Unit Testing:
1// Unit Test: Verifies expected behavior
2func TestReverseString(t *testing.T) {
3 input := "hello"
4 expected := "olleh"
5
6 result := ReverseString(input)
7 if result != expected {
8 t.Errorf("Expected %q, got %q", expected, result)
9 }
10}
11
12// Fuzz Test: Discovers unexpected behavior
13func FuzzReverseString(f *testing.F) {
14 f.Add("hello") // Seed input
15 f.Add("") // Edge case
16 f.Add("a") // Single character
17
18 f.Fuzz(func(t *testing.T, input string) {
19 // Property: Reversing twice should return original
20 reversed := ReverseString(input)
21 doubleReversed := ReverseString(reversed)
22
23 if input != doubleReversed {
24 t.Errorf("Property violated: %q != %q", input, doubleReversed)
25 }
26
27 // Property: Should never panic
28 // Fuzzer will automatically catch panics as failures
29 })
30}
Key Insight: Unit tests ask "Does it work correctly?" Fuzz tests ask "Can I break it?"
Understanding Coverage-Guided Fuzzing
Coverage-guided fuzzing is the secret weapon that makes modern fuzzing effective. Instead of generating completely random inputs, the fuzzer monitors which code paths get executed and intelligently mutates inputs to explore new paths.
How Coverage Guidance Works
1package main
2
3import (
4 "fmt"
5)
6
7// Example function with multiple code paths
8func ClassifyInput(input string) string {
9 // Path 1: Empty string
10 if len(input) == 0 {
11 return "empty"
12 }
13
14 // Path 2: Short string
15 if len(input) < 5 {
16 return "short"
17 }
18
19 // Path 3: Contains "special"
20 if contains(input, "special") {
21 return "special"
22 }
23
24 // Path 4: Long string
25 if len(input) > 100 {
26 return "long"
27 }
28
29 // Path 5: Default
30 return "normal"
31}
32
33func contains(s, substr string) bool {
34 for i := 0; i <= len(s)-len(substr); i++ {
35 if s[i:i+len(substr)] == substr {
36 return true
37 }
38 }
39 return false
40}
41
42// Coverage-guided fuzzing will automatically:
43// 1. Start with seed inputs
44// 2. Monitor which paths are executed
45// 3. Mutate inputs to explore unvisited paths
46// 4. Focus on inputs that increase coverage
47
48func FuzzClassifyInput(f *testing.F) {
49 // Seed corpus - helps fuzzer start
50 f.Add("") // Triggers path 1
51 f.Add("hi") // Triggers path 2
52 f.Add("hello") // Triggers path 5
53 f.Add("special case") // Triggers path 3
54 // Note: Fuzzer will discover path 4 (long) automatically
55
56 f.Fuzz(func(t *testing.T, input string) {
57 // Just call the function - fuzzer tracks coverage
58 result := ClassifyInput(input)
59
60 // Verify properties
61 if result == "empty" && len(input) != 0 {
62 t.Errorf("Classified non-empty as empty")
63 }
64
65 if result == "short" && len(input) >= 5 {
66 t.Errorf("Classified long as short")
67 }
68
69 if result == "special" && !contains(input, "special") {
70 t.Errorf("Classified as special without keyword")
71 }
72 })
73}
74
75func main() {
76 testCases := []string{
77 "",
78 "hi",
79 "hello world",
80 "special case",
81 string(make([]byte, 150)), // Long string
82 }
83
84 fmt.Println("Classification Results:")
85 for _, tc := range testCases {
86 display := tc
87 if len(display) > 30 {
88 display = display[:30] + "..."
89 }
90 result := ClassifyInput(tc)
91 fmt.Printf("Input %-35s -> %s\n", fmt.Sprintf("%q", display), result)
92 }
93}
Mutation Strategies
The fuzzer uses sophisticated mutation strategies to generate interesting inputs:
1package main
2
3import (
4 "fmt"
5 "testing"
6)
7
8// Mutation strategies the fuzzer employs:
9// 1. Bit flipping - flip individual bits
10// 2. Byte insertion - add random bytes
11// 3. Byte deletion - remove bytes
12// 4. Byte substitution - replace bytes
13// 5. Chunk operations - swap, duplicate chunks
14// 6. Dictionary-based mutations - use known tokens
15
16type MutationExample struct {
17 original string
18}
19
20func Demonstrate() {
21 fmt.Println("Original input:", m.original)
22 fmt.Println("\nPossible mutations:")
23
24 // Bit flip
25 fmt.Println("1. Bit flip: ", m.bitFlip())
26
27 // Byte insertion
28 fmt.Println("2. Insert byte: ", m.insertByte())
29
30 // Byte deletion
31 fmt.Println("3. Delete byte: ", m.deleteByte())
32
33 // Byte substitution
34 fmt.Println("4. Substitute: ", m.substitute())
35
36 // Chunk swap
37 fmt.Println("5. Chunk swap: ", m.chunkSwap())
38}
39
40func bitFlip() string {
41 if len(m.original) == 0 {
42 return m.original
43 }
44 bytes := []byte(m.original)
45 bytes[0] ^= 1 // Flip least significant bit
46 return string(bytes)
47}
48
49func insertByte() string {
50 bytes := []byte(m.original)
51 if len(bytes) == 0 {
52 return "X" + m.original
53 }
54 return string(bytes[:1]) + "X" + string(bytes[1:])
55}
56
57func deleteByte() string {
58 if len(m.original) <= 1 {
59 return ""
60 }
61 return m.original[1:]
62}
63
64func substitute() string {
65 if len(m.original) == 0 {
66 return m.original
67 }
68 bytes := []byte(m.original)
69 bytes[0] = 'Z'
70 return string(bytes)
71}
72
73func chunkSwap() string {
74 if len(m.original) < 4 {
75 return m.original
76 }
77 mid := len(m.original) / 2
78 return m.original[mid:] + m.original[:mid]
79}
80
81func main() {
82 example := &MutationExample{original: "hello"}
83 example.Demonstrate()
84
85 fmt.Println("\n--- Coverage-Guided Process ---")
86 fmt.Println("1. Fuzzer starts with 'hello'")
87 fmt.Println("2. Tries 'iello' (bit flip) - new coverage? Keep it")
88 fmt.Println("3. Tries 'Xhello' (insert) - new coverage? Keep it")
89 fmt.Println("4. Repeats with promising mutations")
90 fmt.Println("5. Builds corpus of interesting inputs")
91}
Practical Examples - From Basic to Advanced
Let's start with a simple string reversal function and progressively discover issues through fuzzing.
Example 1: String Reversal - The First Bug
1package main
2
3import (
4 "fmt"
5 "testing"
6)
7
8// StringReverser demonstrates common pitfalls in string processing
9type StringReverser struct{}
10
11func Reverse(input string) string {
12 // Convert to runes
13 runes := []rune(input)
14
15 // Reverse in place
16 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
17 runes[i], runes[j] = runes[j], runes[i]
18 }
19
20 return string(runes)
21}
22
23func ReverseBuggy(input string) string {
24 // BUG: Treats string as bytes
25 bytes := []byte(input)
26
27 for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
28 bytes[i], bytes[j] = bytes[j], bytes[i]
29 }
30
31 return string(bytes) // Corrupts multi-byte Unicode characters
32}
33
34func FuzzStringReverser(f *testing.F) {
35 // Seed corpus: Help fuzzer find interesting cases
36 f.Add("hello") // ASCII
37 f.Add("world") // ASCII
38 f.Add("") // Empty string
39 f.Add("a") // Single character
40 f.Add("hello, 世界") // Mixed ASCII and Unicode
41 f.Add("😀👍") // Emoji
42
43 f.Fuzz(func(t *testing.T, input string) {
44 reverser := &StringReverser{}
45
46 // Test both versions
47 correctResult := reverser.Reverse(input)
48 buggyResult := reverser.ReverseBuggy(input)
49
50 // Property 1: Double reverse should return original
51 doubleCorrect := reverser.Reverse(correctResult)
52 if doubleCorrect != input {
53 t.Errorf("Correct version failed double reverse: %q -> %q -> %q",
54 input, correctResult, doubleCorrect)
55 }
56
57 // Property 2: Results should match for ASCII, differ for Unicode
58 isASCII := func(s string) bool {
59 for _, r := range s {
60 if r > 127 {
61 return false
62 }
63 }
64 return true
65 }
66
67 if isASCII(input) && correctResult != buggyResult {
68 t.Errorf("Results differ for ASCII input %q: correct=%q, buggy=%q",
69 input, correctResult, buggyResult)
70 }
71
72 // Property 3: Length should be preserved
73 if len(correctResult) != len(input) {
74 t.Errorf("Length changed: %d -> %d", len(input), len(correctResult))
75 }
76 })
77}
78
79func main() {
80 reverser := &StringReverser{}
81
82 // Demonstrate the bug
83 testInputs := []string{
84 "hello", // ASCII - works in both
85 "世界", // Unicode - broken in buggy version
86 "café", // Accented characters
87 "👍 hello", // Mixed emoji and text
88 }
89
90 for _, input := range testInputs {
91 correct := reverser.Reverse(input)
92 buggy := reverser.ReverseBuggy(input)
93
94 fmt.Printf("Input: %-15q | Correct: %-15q | Buggy: %-15q\n",
95 input, correct, buggy)
96 }
97}
Fuzzer Output:
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 84723, new interesting: 45
fuzz: elapsed: 6s, execs: 169421, new interesting: 67
--- FAIL: FuzzStringReverser failed after 8s
testing.go:1350: panic: runtime error: slice bounds out of range [:18446744071582087930]
goroutine 6 [running]:
...
Key Finding: The fuzzer quickly discovered that the buggy version corrupts Unicode strings, while the correct version handles them properly.
Example 2: JSON Parser - Finding Security Vulnerabilities
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "testing"
8)
9
10// SafeJSONParser provides secure JSON parsing with validation
11type SafeJSONParser struct {
12 maxDepth int
13 maxElements int
14}
15
16func NewSafeJSONParser() *SafeJSONParser {
17 return &SafeJSONParser{
18 maxDepth: 100, // Prevent stack overflow
19 maxElements: 1000, // Prevent memory exhaustion
20 }
21}
22
23// Parse safely unmarshals JSON with security checks
24func (p *SafeJSONParser) Parse(data []byte) (interface{}, error) {
25 // Security check 1: Input size limit
26 if len(data) > 1024*1024 { // 1MB limit
27 return nil, fmt.Errorf("JSON too large: %d bytes", len(data))
28 }
29
30 // Security check 2: Basic structure validation
31 if p.hasMaliciousPatterns(data) {
32 return nil, fmt.Errorf("potentially malicious JSON detected")
33 }
34
35 var result interface{}
36 decoder := json.NewDecoder(bytes.NewReader(data))
37
38 // Security check 3: Prevent deep nesting during parse
39 decoder.DisallowUnknownFields()
40
41 err := decoder.Decode(&result)
42 if err != nil {
43 return nil, fmt.Errorf("JSON parse error: %w", err)
44 }
45
46 // Security check 4: Validate parsed structure
47 if err := p.validateStructure(result, 0); err != nil {
48 return nil, fmt.Errorf("unsafe JSON structure: %w", err)
49 }
50
51 return result, nil
52}
53
54// hasMaliciousPatterns checks for known attack patterns
55func (p *SafeJSONParser) hasMaliciousPatterns(data []byte) bool {
56 patterns := [][]byte{
57 []byte(`{{`), // Template injection
58 []byte(`${`), // Another template syntax
59 []byte("<script"), // XSS attempt
60 []byte("javascript:"), // Protocol injection
61 }
62
63 for _, pattern := range patterns {
64 if bytes.Contains(data, pattern) {
65 return true
66 }
67 }
68
69 return false
70}
71
72// validateStructure checks for dangerous structures
73func (p *SafeJSONParser) validateStructure(value interface{}, depth int) error {
74 if depth > p.maxDepth {
75 return fmt.Errorf("JSON too deeply nested: %d", depth)
76 }
77
78 switch v := value.(type) {
79 case map[string]interface{}:
80 if len(v) > p.maxElements {
81 return fmt.Errorf("too many object elements: %d", len(v))
82 }
83 for _, val := range v {
84 if err := p.validateStructure(val, depth+1); err != nil {
85 return err
86 }
87 }
88
89 case []interface{}:
90 if len(v) > p.maxElements {
91 return fmt.Errorf("too many array elements: %d", len(v))
92 }
93 for _, val := range v {
94 if err := p.validateStructure(val, depth+1); err != nil {
95 return err
96 }
97 }
98
99 case string:
100 if len(v) > p.maxElements {
101 return fmt.Errorf("string too long: %d characters", len(v))
102 }
103 }
104
105 return nil
106}
107
108// VulnerableJSONParser represents a vulnerable parser for comparison
109type VulnerableJSONParser struct{}
110
111func (v *VulnerableJSONParser) Parse(data []byte) (interface{}, error) {
112 // Vulnerable: No security checks
113 var result interface{}
114 err := json.Unmarshal(data, &result)
115 return result, err
116}
117
118func FuzzJSONParsers(f *testing.F) {
119 // Seed corpus with various JSON patterns
120 f.Add([]byte(`{"name":"Alice","age":30}`))
121 f.Add([]byte(`{"nested":{"deep":{"structure":{}}}}`))
122 f.Add([]byte(`{"array":[1,2,3,4,5]}`))
123 f.Add([]byte(`"simple string"`))
124 f.Add([]byte(`null`))
125 f.Add([]byte(`[]`))
126 f.Add([]byte(`{}`))
127
128 // Add potentially dangerous inputs
129 f.Add([]byte(`{"template":"{{.user}}"}`))
130 f.Add([]byte(`{"script":"<script>alert('xss')</script>"}`))
131
132 f.Fuzz(func(t *testing.T, data []byte) {
133 safeParser := NewSafeJSONParser()
134 vulnerableParser := &VulnerableJSONParser{}
135
136 // Test safe parser
137 safeResult, safeErr := safeParser.Parse(data)
138
139 // Test vulnerable parser
140 vulnerableResult, vulnerableErr := vulnerableParser.Parse(data)
141
142 // If both succeed, results should match for valid JSON
143 if safeErr == nil && vulnerableErr == nil {
144 // Round-trip test for safe parser
145 remarshaled, err := json.Marshal(safeResult)
146 if err != nil {
147 t.Errorf("Safe parser result cannot be remarshaled: %v", err)
148 }
149
150 // Re-parse should work
151 _, err = safeParser.Parse(remarshaled)
152 if err != nil {
153 t.Errorf("Round-trip failed: %v", err)
154 }
155 }
156
157 // Safe parser should reject malicious inputs that vulnerable parser accepts
158 if safeErr != nil && vulnerableErr == nil {
159 t.Logf("Safe parser correctly rejected potentially malicious JSON: %v", safeErr)
160 }
161
162 // Properties to verify when parsing succeeds:
163 if safeErr == nil {
164 // 1. Result should not be nil for non-null JSON
165 if safeResult == nil && !bytes.Equal(data, []byte("null")) {
166 t.Error("Parser returned nil for non-null JSON")
167 }
168
169 // 2. Should be able to marshal the result
170 _, err := json.Marshal(safeResult)
171 if err != nil {
172 t.Errorf("Cannot marshal parsing result: %v", err)
173 }
174 }
175 })
176}
177
178func main() {
179 parser := NewSafeJSONParser()
180
181 testCases := []struct {
182 name string
183 data []byte
184 shouldPass bool
185 }{
186 {"Valid JSON", []byte(`{"name":"Alice","age":30}`), true},
187 {"Deep nesting", []byte(`{"a":` + repeat(`{"b":`, 200) + `"x"` + repeat(`}`, 200) + `}`), false},
188 {"Large array", []byte(`[` + repeat(`"x",`, 2000) + `"x"]`), false},
189 {"Malicious template", []byte(`{"template":"{{.user}}"}`), false},
190 {"XSS attempt", []byte(`{"script":"<script>alert(1)</script>"}`), false},
191 }
192
193 for _, tc := range testCases {
194 result, err := parser.Parse(tc.data)
195 passed := err == nil
196 status := "✅"
197 if passed != tc.shouldPass {
198 status = "❌"
199 }
200
201 fmt.Printf("%s %-20s: %v\n", status, tc.name, passed)
202 if passed {
203 fmt.Printf(" Result: %+v\n", result)
204 } else {
205 fmt.Printf(" Error: %v\n", err)
206 }
207 }
208}
209
210func repeat(s string, count int) string {
211 result := make([]byte, 0, len(s)*count)
212 for i := 0; i < count; i++ {
213 result = append(result, s...)
214 }
215 return string(result)
216}
Fuzzer Discoveries:
fuzz: elapsed: 5s, execs: 127431, new interesting: 89
fuzz: elapsed: 10s, execs: 254892, new interesting: 124
--- FAIL: FuzzJSONParsers failed after 12s
testing.go:1350: memory allocation size 1073741824 exceeds limit
goroutine 7 [running]:
...
Example 3: Rate Limiter - Finding Race Conditions
1package main
2
3import (
4 "fmt"
5 "sync"
6 "sync/atomic"
7 "testing"
8 "time"
9)
10
11// RateLimiter implements token bucket with thread safety
12type RateLimiter struct {
13 capacity int64
14 tokens int64 // Atomic
15 refillRate int64 // tokens per second
16 lastRefill int64 // Unix nanoseconds, atomic
17 mu sync.Mutex
18}
19
20func NewRateLimiter(capacity, refillRate int64) *RateLimiter {
21 now := time.Now().UnixNano()
22 return &RateLimiter{
23 capacity: capacity,
24 tokens: capacity,
25 refillRate: refillRate,
26 lastRefill: now,
27 }
28}
29
30// Allow checks if a request should be allowed
31func (rl *RateLimiter) Allow() bool {
32 // Refill tokens based on elapsed time
33 rl.refill()
34
35 // Try to consume a token atomically
36 for {
37 current := atomic.LoadInt64(&rl.tokens)
38 if current <= 0 {
39 return false
40 }
41
42 // Compare and swap
43 if atomic.CompareAndSwapInt64(&rl.tokens, current, current-1) {
44 return true
45 }
46 // Retry if CAS failed
47 }
48}
49
50// refill updates the token count based on elapsed time
51func (rl *RateLimiter) refill() {
52 now := time.Now().UnixNano()
53 last := atomic.LoadInt64(&rl.lastRefill)
54
55 // Try to update lastRefill time
56 if atomic.CompareAndSwapInt64(&rl.lastRefill, last, now) {
57 // We won the race, calculate tokens to add
58 elapsed := now - last
59 tokensToAdd := (elapsed * rl.refillRate) / int64(time.Second)
60
61 if tokensToAdd > 0 {
62 for {
63 current := atomic.LoadInt64(&rl.tokens)
64 newTokens := current + tokensToAdd
65
66 // Don't exceed capacity
67 if newTokens > rl.capacity {
68 newTokens = rl.capacity
69 }
70
71 if atomic.CompareAndSwapInt64(&rl.tokens, current, newTokens) {
72 break
73 }
74 // Retry if CAS failed
75 }
76 }
77 }
78}
79
80// GetTokens returns current token count
81func (rl *RateLimiter) GetTokens() int64 {
82 rl.refill()
83 return atomic.LoadInt64(&rl.tokens)
84}
85
86func FuzzRateLimiter(f *testing.F) {
87 // Seed with common rate limiter scenarios
88 f.Add(int64(10), int64(5), int64(100)) // capacity, rate, requests
89 f.Add(int64(100), int64(10), int64(1000))
90 f.Add(int64(1), int64(1), int64(50))
91
92 f.Fuzz(func(t *testing.T, capacity, rate, numRequests int64) {
93 // Bounds checking
94 if capacity <= 0 || capacity > 1000 {
95 return
96 }
97 if rate <= 0 || rate > 1000 {
98 return
99 }
100 if numRequests <= 0 || numRequests > 1000 {
101 return
102 }
103
104 rl := NewRateLimiter(capacity, rate)
105
106 var allowed, denied int64
107
108 // Test concurrent access
109 var wg sync.WaitGroup
110 numWorkers := int64(10)
111 requestsPerWorker := numRequests / numWorkers
112
113 for i := int64(0); i < numWorkers; i++ {
114 wg.Add(1)
115 go func(workerID int64) {
116 defer wg.Done()
117
118 for j := int64(0); j < requestsPerWorker; j++ {
119 if rl.Allow() {
120 atomic.AddInt64(&allowed, 1)
121 } else {
122 atomic.AddInt64(&denied, 1)
123 }
124
125 // Small delay to allow refilling
126 time.Sleep(time.Microsecond)
127 }
128 }(i)
129 }
130
131 wg.Wait()
132
133 // Verify invariants
134 totalRequests := allowed + denied
135 if totalRequests != numRequests {
136 t.Errorf("Request count mismatch: %d + %d != %d",
137 allowed, denied, numRequests)
138 }
139
140 // Tokens should never be negative
141 tokens := rl.GetTokens()
142 if tokens < 0 {
143 t.Errorf("Negative tokens: %d", tokens)
144 }
145
146 // Tokens should never exceed capacity
147 if tokens > capacity {
148 t.Errorf("Tokens exceed capacity: %d > %d", tokens, capacity)
149 }
150
151 // Should not allow more requests than capacity initially
152 if allowed > capacity && numRequests >= capacity {
153 t.Errorf("Allowed more than capacity: %d > %d", allowed, capacity)
154 }
155 })
156}
157
158func main() {
159 // Demonstrate rate limiter behavior
160 rl := NewRateLimiter(5, 2) // 5 tokens, 2 tokens/second
161
162 fmt.Println("Rate Limiter Demo:")
163
164 for i := 0; i < 15; i++ {
165 allowed := rl.Allow()
166 tokens := rl.GetTokens()
167
168 status := "❌ DENIED"
169 if allowed {
170 status = "✅ ALLOWED"
171 }
172
173 fmt.Printf("Request %2d: %-10s | Tokens: %d\n", i+1, status, tokens)
174
175 if i%5 == 4 {
176 fmt.Println(" [Waiting 1 second for refill...]")
177 time.Sleep(1 * time.Second)
178 }
179 }
180}
Corpus Management and Optimization
Effective corpus management is crucial for finding bugs efficiently. The corpus is the collection of inputs that the fuzzer uses as a starting point.
Building an Effective Seed Corpus
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "testing"
9)
10
11// CorpusManager handles test corpus for fuzzing
12type CorpusManager struct {
13 corpusDir string
14}
15
16func NewCorpusManager(dir string) *CorpusManager {
17 return &CorpusManager{corpusDir: dir}
18}
19
20// SaveSeed saves a seed input to corpus
21func (cm *CorpusManager) SaveSeed(name string, data []byte) error {
22 if err := os.MkdirAll(cm.corpusDir, 0755); err != nil {
23 return fmt.Errorf("create corpus dir: %w", err)
24 }
25
26 path := filepath.Join(cm.corpusDir, name)
27 if err := os.WriteFile(path, data, 0644); err != nil {
28 return fmt.Errorf("write seed: %w", err)
29 }
30
31 return nil
32}
33
34// LoadSeeds loads all seeds from corpus directory
35func (cm *CorpusManager) LoadSeeds() ([][]byte, error) {
36 entries, err := os.ReadDir(cm.corpusDir)
37 if err != nil {
38 return nil, fmt.Errorf("read corpus dir: %w", err)
39 }
40
41 var seeds [][]byte
42 for _, entry := range entries {
43 if entry.IsDir() {
44 continue
45 }
46
47 path := filepath.Join(cm.corpusDir, entry.Name())
48 data, err := os.ReadFile(path)
49 if err != nil {
50 continue
51 }
52
53 seeds = append(seeds, data)
54 }
55
56 return seeds, nil
57}
58
59// Example: Building corpus for URL parser
60func BuildURLCorpus() []string {
61 return []string{
62 // Valid URLs
63 "http://example.com",
64 "https://example.com",
65 "https://example.com:8080",
66 "https://user:pass@example.com",
67 "https://example.com/path",
68 "https://example.com/path?query=value",
69 "https://example.com/path?q1=v1&q2=v2",
70 "https://example.com/path#fragment",
71 "https://example.com:8080/path?query=value#fragment",
72
73 // Edge cases
74 "", // Empty
75 "://no-scheme.com", // Missing scheme
76 "http://", // No host
77 "http://example.com:abc", // Invalid port
78 "http://example.com:-1", // Negative port
79 "http://example.com:999999", // Port too large
80 "http://[invalid-ipv6]", // Malformed IPv6
81 "http://256.256.256.256", // Invalid IPv4
82 "http://example.com//double/slash", // Double slashes
83 "http://example.com/path/../../../etc/passwd", // Path traversal
84
85 // Special characters
86 "http://example.com/path with spaces",
87 "http://example.com/path%20encoded",
88 "http://example.com/unicode/世界",
89 "http://example.com/emoji/👍",
90 "http://example.com/path?query=<script>",
91
92 // International domain names
93 "http://münchen.de",
94 "http://中国.cn",
95
96 // Long inputs
97 "http://example.com/" + string(make([]byte, 1000)),
98 "http://" + string(make([]byte, 1000)) + ".com",
99 }
100}
101
102// Example: Building corpus for JSON parser
103func BuildJSONCorpus() []string {
104 return []string{
105 // Valid JSON
106 `{}`,
107 `[]`,
108 `null`,
109 `true`,
110 `false`,
111 `123`,
112 `"string"`,
113 `{"key":"value"}`,
114 `{"nested":{"object":true}}`,
115 `[1,2,3]`,
116 `[[[]]]`,
117
118 // Edge cases
119 ``, // Empty
120 `{`, // Incomplete
121 `}`, // Just closing
122 `{{}`, // Unbalanced
123 `{"key":}`, // Missing value
124 `{"key":"value",}`, // Trailing comma
125 `{key:"value"}`, // Unquoted key
126
127 // Deep nesting
128 repeat(`[`, 100) + repeat(`]`, 100),
129 repeat(`{"a":`, 100) + `null` + repeat(`}`, 100),
130
131 // Large values
132 `{"large":"` + string(make([]byte, 10000)) + `"}`,
133 `[` + repeat(`1,`, 1000) + `1]`,
134
135 // Special characters
136 `{"emoji":"👍"}`,
137 `{"unicode":"世界"}`,
138 `{"escaped":"\"quotes\""}`,
139 `{"newline":"line1\nline2"}`,
140 }
141}
142
143func repeat(s string, n int) string {
144 result := ""
145 for i := 0; i < n; i++ {
146 result += s
147 }
148 return result
149}
150
151// FuzzWithManagedCorpus demonstrates corpus management
152func FuzzWithManagedCorpus(f *testing.F) {
153 // Load existing corpus
154 manager := NewCorpusManager("testdata/fuzz/corpus")
155 seeds, err := manager.LoadSeeds()
156 if err == nil {
157 for _, seed := range seeds {
158 f.Add(seed)
159 }
160 }
161
162 // Add programmatic seeds
163 for _, urlStr := range BuildURLCorpus() {
164 f.Add([]byte(urlStr))
165 }
166
167 f.Fuzz(func(t *testing.T, data []byte) {
168 // Your fuzzing logic here
169 _ = data
170 })
171}
172
173func main() {
174 fmt.Println("=== URL Corpus Examples ===")
175 urlCorpus := BuildURLCorpus()
176 for i, url := range urlCorpus[:10] { // Show first 10
177 fmt.Printf("%2d. %s\n", i+1, url)
178 }
179
180 fmt.Println("\n=== JSON Corpus Examples ===")
181 jsonCorpus := BuildJSONCorpus()
182 for i, jsonStr := range jsonCorpus[:10] { // Show first 10
183 fmt.Printf("%2d. %s\n", i+1, jsonStr)
184 }
185
186 fmt.Printf("\nTotal URL corpus size: %d\n", len(urlCorpus))
187 fmt.Printf("Total JSON corpus size: %d\n", len(jsonCorpus))
188}
Common Patterns and Pitfalls
Pattern 1: Property-Based Testing
The most effective fuzz tests verify properties that should always be true:
1// Good: Test invariants
2func FuzzStringOperations(f *testing.F) {
3 f.Fuzz(func(t *testing.T, a, b string) {
4 // Property: Contains should be consistent with Index
5 if strings.Contains(a, b) {
6 if strings.Index(a, b) == -1 {
7 t.Errorf("Contains true but Index returns -1")
8 }
9 }
10
11 // Property: String length preserved by double reverse
12 doubleReversed := reverse(reverse(a))
13 if len(doubleReversed) != len(a) {
14 t.Errorf("Length changed by double reverse")
15 }
16
17 // Property: Join should increase length by at least separators
18 joined := strings.Join([]string{a, b}, ",")
19 if len(joined) < len(a)+len(b) {
20 t.Errorf("Join result too short")
21 }
22 })
23}
24
25// Bad: Test specific outcomes
26func FuzzBadExample(f *testing.F) {
27 f.Fuzz(func(t *testing.T, input string) {
28 // This is just unit testing with random inputs
29 result := process(input)
30 if result != "expected" { // What's "expected" for random input?
31 t.Errorf("Wrong result")
32 }
33 })
34}
Pattern 2: Progressive Complexity
Start simple, then add complexity:
1// Level 1: Basic functionality
2func FuzzBasic(f *testing.F) {
3 f.Fuzz(func(t *testing.T, input string) {
4 // Should never panic
5 _ = basicFunction(input)
6 })
7}
8
9// Level 2: Add invariants
10func FuzzWithInvariants(f *testing.F) {
11 f.Fuzz(func(t *testing.T, input string) {
12 result := basicFunction(input)
13
14 // Should satisfy basic properties
15 if len(result) > maxExpectedLength {
16 t.Errorf("Result too long")
17 }
18 })
19}
20
21// Level 3: Complex scenarios
22func FuzzComplex(f *testing.F) {
23 f.Fuzz(func(t *testing.T, input1, input2 string) {
24 // Test interaction between multiple inputs
25 result1 := basicFunction(input1)
26 result2 := basicFunction(input2)
27
28 combined := combine(result1, result2)
29 if violatesInvariant(combined) {
30 t.Errorf("Combined input breaks invariant")
31 }
32 })
33}
Common Pitfalls
- Testing Implementation Details:
1// Bad: Tests internal implementation
2if bytes.Contains(result, []byte("internal_variable")) {
3 t.Error("Contains internal variable name")
4}
5
6// Good: Tests external behavior
7if !isValidOutput(result) {
8 t.Error("Invalid output format")
9}
- Assuming Input Distribution:
1// Bad: Assumes ASCII input
2func isSpecial(char byte) bool {
3 return char < 32 // Fails with UTF-8
4}
5
6// Good: Handles all inputs
7func isSpecial(r rune) bool {
8 return r < 32 || r > 126
9}
- Infinite Loops on Bad Input:
1// Bad: Can loop forever on malformed input
2for !isValid(input) {
3 input = transform(input) // What if transform never makes it valid?
4}
5
6// Good: Limits iterations
7for i := 0; i < 1000 && !isValid(input); i++ {
8 input = transform(input)
9}
Integration and Mastery - Production Fuzzing
Setting Up Continuous Fuzzing
1# .github/workflows/fuzz.yml
2name: Continuous Fuzzing
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 schedule:
9 - cron: '0 2 * * *' # Daily at 2 AM
10
11jobs:
12 fuzz:
13 runs-on: ubuntu-latest
14 strategy:
15 matrix:
16 target:
17 - FuzzStringReverser
18 - FuzzJSONParsers
19 - FuzzRateLimiter
20 time:
21 - 30s
22 - 2m
23 - 5m
24
25 steps:
26 - uses: actions/checkout@v3
27
28 - name: Set up Go
29 uses: actions/setup-go@v4
30 with:
31 go-version: '1.21'
32
33 - name: Cache go modules
34 uses: actions/cache@v3
35 with:
36 path: ~/go/pkg/mod
37 key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
38
39 - name: Run fuzzing
40 run: |
41 set -eo pipefail
42
43 echo "Starting fuzz test: ${{ matrix.target }}"
44 echo "Duration: ${{ matrix.time }}"
45
46 # Run with race detector for concurrent code
47 go test -race \
48 -fuzz=${{ matrix.target }} \
49 -fuzztime=${{ matrix.time }} \
50 -parallel=4 \
51 ./...
52
53 echo "Fuzzing completed successfully"
54
55 - name: Upload corpus
56 if: always()
57 uses: actions/upload-artifact@v3
58 with:
59 name: fuzz-corpus-${{ matrix.target }}
60 path: testdata/fuzz/${{ matrix.target }}/
61 retention-days: 30
Corpus Management Best Practices
1// corpus_test.go
2package mypackage
3
4import (
5 "testing"
6 "time"
7)
8
9func FuzzMyFunction(f *testing.F) {
10 // 1. Seed with diverse, realistic inputs
11 f.Add([]byte("valid_json_string"))
12 f.Add([]byte("")) // Empty
13 f.Add([]byte("{")) // Malformed start
14 f.Add([]byte("}")) // Malformed end
15 f.Add(make([]byte, 10000)) // Large input
16 f.Add([]byte{0x00, 0xFF, 0xFE}) // Binary data
17
18 // 2. Add real-world examples
19 for _, example := range realWorldExamples {
20 f.Add(example)
21 }
22
23 f.Fuzz(func(t *testing.T, data []byte) {
24 // Early bounds checking improves performance
25 if len(data) > maxInputSize {
26 return // Skip oversized inputs
27 }
28
29 // Test the function
30 result := MyFunction(data)
31
32 // Verify properties
33 if result != nil {
34 // Basic validation
35 if len(result.Output) > maxOutputSize {
36 t.Errorf("Output too large: %d bytes", len(result.Output))
37 }
38
39 // Round-trip test if applicable
40 if result.CanRoundTrip() {
41 roundTrip := result.ReverseProcess()
42 if !roundTrip.Equals(data) {
43 t.Errorf("Round-trip failed")
44 }
45 }
46 }
47 })
48}
49
50// Helper: Generate realistic test data
51func generateRealWorldExamples() [][]byte {
52 return [][]byte{
53 []byte(`{"name":"John","age":30}`), // JSON object
54 []byte(`[1,2,3,4,5]`), // JSON array
55 []byte("user@example.com"), // Email format
56 []byte("HTTP/1.1 200 OK\r\n..."), // HTTP response
57 []byte("<?xml version='1.0'?>..."), // XML document
58 }
59}
Fuzzing Different Types of Code
1. Parsers and Data Processors
1func FuzzCSVParser(f *testing.F) {
2 f.Add("name,age,city\nJohn,30,NYC")
3 f.Add("a,b,c\n1,2,3\n4,5,6")
4 f.Add("")
5
6 f.Fuzz(func(t *testing.T, csvData string) {
7 parser := NewCSVParser()
8
9 // Should never panic on any input
10 records, err := parser.Parse(csvData)
11
12 if err == nil {
13 // Verify invariants when parsing succeeds
14 if len(records) == 0 && csvData != "" {
15 t.Error("Empty result from non-empty CSV")
16 }
17
18 // All records should have same number of fields
19 if len(records) > 1 {
20 fieldCount := len(records[0])
21 for i, record := range records[1:] {
22 if len(record) != fieldCount {
23 t.Errorf("Record %d has %d fields, expected %d",
24 i+1, len(record), fieldCount)
25 }
26 }
27 }
28 }
29 })
30}
2. Network Protocol Handlers
1func FuzzHTTPHandler(f *testing.F) {
2 f.Add([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
3 f.Add([]byte("POST /api HTTP/1.1\r\nContent-Length: 10\r\n\r\n1234567890"))
4 f.Add([]byte("INVALID REQUEST"))
5
6 f.Fuzz(func(t *testing.T, requestData []byte) {
7 // Create request from fuzzed data
8 req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(requestData)))
9 if err != nil {
10 // Malformed request is acceptable
11 return
12 }
13
14 // Test handler with request
15 rr := httptest.NewRecorder()
16 MyHTTPHandler(rr, req)
17
18 // Response should be valid HTTP
19 resp := rr.Result()
20 if resp.StatusCode < 100 || resp.StatusCode > 599 {
21 t.Errorf("Invalid status code: %d", resp.StatusCode)
22 }
23
24 // Should not panic or hang
25 if resp.Body == nil {
26 t.Error("Response body is nil")
27 }
28 })
29}
3. Database Operations
1func FuzzSQLBuilder(f *testing.F) {
2 f.Add("users", "name", "Alice")
3 f.Add("orders", "id", "12345")
4
5 f.Fuzz(func(t *testing.T, table, column, value string) {
6 // Validate inputs to prevent SQL injection in the fuzzer itself
7 if !isValidIdentifier(table) || !isValidIdentifier(column) {
8 return
9 }
10
11 // Build query safely
12 query := SQLBuilder{}
13 query.Select(column).From(table).Where("name = ?", value)
14
15 sql, args, err := query.Build()
16 if err != nil {
17 return // Build errors are acceptable
18 }
19
20 // Generated SQL should be parameterized
21 if !bytes.Contains(sql, []byte("?")) {
22 t.Errorf("Query not parameterized: %s", sql)
23 }
24
25 // Args should match placeholder count
26 placeholderCount := bytes.Count(sql, []byte("?"))
27 if len(args) != placeholderCount {
28 t.Errorf("Args count mismatch: %d args, %d placeholders",
29 len(args), placeholderCount)
30 }
31 })
32}
Practice Exercises
Exercise 1: Template Engine Fuzzer
Build a simple template engine and write fuzz tests to find security vulnerabilities.
Requirements:
- Template syntax:
Hello {{.name}}! - Support nested objects:
{{.user.name}} - Handle missing fields gracefully
- Prevent infinite recursion
- Block template injection attacks
Solution
1package main
2
3import (
4 "fmt"
5 "regexp"
6 "strings"
7 "testing"
8)
9
10// TemplateEngine processes simple template syntax
11type TemplateEngine struct {
12 maxRecursion int
13}
14
15func NewTemplateEngine() *TemplateEngine {
16 return &TemplateEngine{maxRecursion: 10}
17}
18
19// Execute renders a template with given data
20func (te *TemplateEngine) Execute(template string, data map[string]interface{}) (string, error) {
21 return te.executeWithDepth(template, data, 0)
22}
23
24func (te *TemplateEngine) executeWithDepth(template string, data map[string]interface{}, depth int) (string, error) {
25 if depth > te.maxRecursion {
26 return "", fmt.Errorf("template recursion too deep")
27 }
28
29 // Security check: Template size limit
30 if len(template) > 10000 {
31 return "", fmt.Errorf("template too large")
32 }
33
34 // Find all {{.field}} patterns
35 re := regexp.MustCompile(`\{\{\.([^}]+)\}\}`)
36 result := template
37
38 for {
39 matches := re.FindStringSubmatchIndex(result)
40 if len(matches) == 0 {
41 break
42 }
43
44 fullMatch := result[matches[0]:matches[1]]
45 fieldPath := result[matches[2]:matches[3]]
46
47 // Security check: Validate field path
48 if err := te.validateFieldPath(fieldPath); err != nil {
49 return "", fmt.Errorf("invalid field path %q: %w", fieldPath, err)
50 }
51
52 value := te.getValue(data, fieldPath)
53 valueStr := te.interfaceToString(value)
54
55 // Recursive expansion for nested templates
56 expanded, err := te.executeWithDepth(valueStr, data, depth+1)
57 if err != nil {
58 return "", err
59 }
60
61 result = result[:matches[0]] + expanded + result[matches[1]:]
62 }
63
64 return result, nil
65}
66
67// validateFieldPath checks for dangerous field paths
68func (te *TemplateEngine) validateFieldPath(path string) error {
69 // Security: Check for injection patterns
70 dangerousPatterns := []string{
71 "{{", "}}", "<script", "javascript:",
72 "data:", "vbscript:", "onload=", "onerror=",
73 }
74
75 lowerPath := strings.ToLower(path)
76 for _, pattern := range dangerousPatterns {
77 if strings.Contains(lowerPath, pattern) {
78 return fmt.Errorf("potentially dangerous pattern: %s", pattern)
79 }
80 }
81
82 // Security: Check path length
83 if len(path) > 100 {
84 return fmt.Errorf("field path too long")
85 }
86
87 return nil
88}
89
90// getValue retrieves value from data using dot notation
91func (te *TemplateEngine) getValue(data map[string]interface{}, path string) interface{} {
92 parts := strings.Split(path, ".")
93 current := data
94
95 for _, part := range parts {
96 if part == "" {
97 return nil
98 }
99
100 if value, ok := current[part]; ok {
101 if nextMap, ok := value.(map[string]interface{}); ok {
102 current = nextMap
103 } else {
104 return value
105 }
106 } else {
107 return nil
108 }
109 }
110
111 return current
112}
113
114func (te *TemplateEngine) interfaceToString(value interface{}) string {
115 switch v := value.(type) {
116 case string:
117 return v
118 case int, int64, float64:
119 return fmt.Sprintf("%v", v)
120 case bool:
121 if v {
122 return "true"
123 }
124 return "false"
125 default:
126 if value == nil {
127 return ""
128 }
129 return fmt.Sprintf("%v", v)
130 }
131}
132
133func FuzzTemplateEngine(f *testing.F) {
134 // Seed corpus
135 f.Add("Hello {{.name}}!")
136 f.Add("{{.user.name}} is {{.user.age}} years old")
137 f.Add("No placeholders here")
138 f.Add("{{.missing}}")
139
140 // Dangerous inputs
141 f.Add("{{.user.<script>alert('xss')</script>}}")
142 f.Add("{{.data:dangerous}}")
143
144 f.Fuzz(func(t *testing.T, template string) {
145 engine := NewTemplateEngine()
146
147 data := map[string]interface{}{
148 "name": "Test",
149 "user": map[string]interface{}{
150 "name": "Alice",
151 "age": 30,
152 },
153 }
154
155 result, err := engine.Execute(template, data)
156
157 // If execution succeeds, verify properties
158 if err == nil {
159 // 1. Result should not contain unprocessed templates
160 if strings.Contains(result, "{{.") {
161 t.Errorf("Result contains unprocessed template: %q", result)
162 }
163
164 // 2. Result should not be too large
165 if len(result) > 100000 {
166 t.Errorf("Result too large: %d characters", len(result))
167 }
168
169 // 3. Should not contain dangerous HTML
170 dangerous := []string{"<script", "javascript:", "data:"}
171 lowerResult := strings.ToLower(result)
172 for _, d := range dangerous {
173 if strings.Contains(lowerResult, d) {
174 t.Errorf("Result contains dangerous content: %s", d)
175 }
176 }
177
178 // 4. Execution should be idempotent
179 result2, err2 := engine.Execute(template, data)
180 if err2 != nil {
181 t.Errorf("Second execution failed: %v", err2)
182 }
183 if result != result2 {
184 t.Errorf("Execution not idempotent")
185 }
186 }
187 })
188}
189
190func main() {
191 engine := NewTemplateEngine()
192
193 template := "Hello {{.user.name}}! You are {{.user.age}} years old."
194 data := map[string]interface{}{
195 "user": map[string]interface{}{
196 "name": "Alice",
197 "age": 30,
198 },
199 }
200
201 result, err := engine.Execute(template, data)
202 if err != nil {
203 fmt.Printf("Error: %v\n", err)
204 } else {
205 fmt.Printf("Result: %s\n", result)
206 }
207
208 // Test dangerous input
209 dangerous := "{{.user.<script>alert('xss')</script>}}"
210 _, err = engine.Execute(dangerous, nil)
211 if err != nil {
212 fmt.Printf("Dangerous input blocked: %v\n", err)
213 }
214}
Fuzzer Discoveries:
The fuzzer helps find:
- Template injection vulnerabilities
- Infinite recursion through circular references
- Memory exhaustion through large outputs
- XSS attacks through user input
Exercise 2: Binary Parser Fuzzer
Create a binary parser with proper security controls and fuzz it to find vulnerabilities.
Solution
1package main
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "testing"
9)
10
11// BinaryParser parses a custom binary format
12type BinaryParser struct {
13 maxElements int
14 maxDepth int
15}
16
17type Record struct {
18 ID uint32
19 Type uint8
20 Value []byte
21}
22
23func NewBinaryParser() *BinaryParser {
24 return &BinaryParser{
25 maxElements: 1000,
26 maxDepth: 10,
27 }
28}
29
30// Parse safely parses binary data into records
31func (bp *BinaryParser) Parse(data []byte) ([]Record, error) {
32 // Security check: Input size
33 if len(data) > 1024*1024 {
34 return nil, errors.New("input too large")
35 }
36
37 if len(data) < 10 {
38 return nil, errors.New("insufficient data for header")
39 }
40
41 // Read header
42 var header struct {
43 Magic uint32
44 Version uint8
45 Count uint16
46 Reserved [3]uint8
47 }
48
49 if err := binary.Read(bytes.NewReader(data[:10]), binary.BigEndian, &header); err != nil {
50 return nil, fmt.Errorf("header parse error: %w", err)
51 }
52
53 // Validate header
54 if header.Magic != 0xDEADBEEF {
55 return nil, errors.New("invalid magic number")
56 }
57
58 if header.Version != 1 {
59 return nil, fmt.Errorf("unsupported version: %d", header.Version)
60 }
61
62 if int(header.Count) > bp.maxElements {
63 return nil, fmt.Errorf("too many elements: %d", header.Count)
64 }
65
66 // Calculate expected data size
67 expectedSize := 10 // header size
68 for i := uint16(0); i < header.Count; i++ {
69 recordSize := 4 + 1 + 2 // ID + Type + ValueLength
70 if int(expectedSize)+recordSize > len(data) {
71 return nil, errors.New("insufficient data for records")
72 }
73
74 // Read record header
75 offset := int(expectedSize)
76 valueLength := binary.BigEndian.Uint16(data[offset+5 : offset+7])
77
78 if valueLength > 1000 {
79 return nil, fmt.Errorf("record value too large: %d", valueLength)
80 }
81
82 if int(expectedSize)+recordSize+int(valueLength) > len(data) {
83 return nil, errors.New("insufficient data for value")
84 }
85
86 expectedSize += recordSize + int(valueLength)
87 }
88
89 // Parse records
90 var records []Record
91 offset := 10
92
93 for i := uint16(0); i < header.Count; i++ {
94 if offset+7 > len(data) {
95 break
96 }
97
98 id := binary.BigEndian.Uint32(data[offset : offset+4])
99 recordType := data[offset+4]
100 valueLength := binary.BigEndian.Uint16(data[offset+5 : offset+7])
101
102 if int(offset)+7+int(valueLength) > len(data) {
103 break
104 }
105
106 value := make([]byte, valueLength)
107 copy(value, data[offset+7:offset+7+int(valueLength)])
108
109 records = append(records, Record{
110 ID: id,
111 Type: recordType,
112 Value: value,
113 })
114
115 offset += 7 + int(valueLength)
116 }
117
118 return records, nil
119}
120
121func FuzzBinaryParser(f *testing.F) {
122 // Seed corpus
123 f.Add(createValidBinary([]Record{{ID: 1, Type: 1, Value: []byte("hello")}}))
124 f.Add(createValidBinary([]Record{}))
125 f.Add([]byte{}) // Empty input
126
127 f.Fuzz(func(t *testing.T, data []byte) {
128 parser := NewBinaryParser()
129
130 // Should never panic
131 records, err := parser.Parse(data)
132
133 if err == nil {
134 // Verify invariants when parsing succeeds
135
136 // 1. Record count should be reasonable
137 if len(records) > 1000 {
138 t.Errorf("Too many records parsed: %d", len(records))
139 }
140
141 // 2. Each record should be valid
142 for i, record := range records {
143 if record.Type > 10 {
144 t.Errorf("Record %d has invalid type: %d", i, record.Type)
145 }
146
147 if len(record.Value) > 1000 {
148 t.Errorf("Record %d has value too long: %d", i, len(record.Value))
149 }
150 }
151
152 // 3. Should be able to reconstruct similar binary
153 if len(records) > 0 {
154 reconstructed := createValidBinary(records)
155 if len(reconstructed) > 100000 {
156 t.Errorf("Reconstructed binary too large: %d", len(reconstructed))
157 }
158 }
159 }
160 })
161}
162
163func createValidBinary(records []Record) []byte {
164 // Helper to create valid binary format for testing
165 buf := make([]byte, 10)
166 binary.BigEndian.PutUint32(buf[0:4], 0xDEADBEEF)
167 buf[4] = 1 // version
168 binary.BigEndian.PutUint16(buf[5:7], uint16(len(records)))
169
170 for _, record := range records {
171 header := make([]byte, 7)
172 binary.BigEndian.PutUint32(header[0:4], record.ID)
173 header[4] = record.Type
174 binary.BigEndian.PutUint16(header[5:7], uint16(len(record.Value)))
175
176 buf = append(buf, header...)
177 buf = append(buf, record.Value...)
178 }
179
180 return buf
181}
182
183func main() {
184 parser := NewBinaryParser()
185
186 // Test valid binary
187 records := []Record{
188 {ID: 1, Type: 1, Value: []byte("hello")},
189 {ID: 2, Type: 2, Value: []byte("world")},
190 }
191
192 data := createValidBinary(records)
193 parsed, err := parser.Parse(data)
194 if err != nil {
195 fmt.Printf("Error: %v\n", err)
196 } else {
197 fmt.Printf("Parsed %d records:\n", len(parsed))
198 for _, record := range parsed {
199 fmt.Printf(" ID: %d, Type: %d, Value: %s\n",
200 record.ID, record.Type, record.Value)
201 }
202 }
203
204 // Test invalid magic number
205 invalid := make([]byte, 10)
206 binary.BigEndian.PutUint32(invalid[0:4], 0x12345678) // Wrong magic
207 _, err = parser.Parse(invalid)
208 if err != nil {
209 fmt.Printf("Invalid magic correctly rejected: %v\n", err)
210 }
211}
Exercise 3: URL Router Fuzzer
Build a URL router with parameter extraction and test it with fuzzing to ensure security and correctness.
Solution
1package main
2
3import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "regexp"
8 "strings"
9 "testing"
10)
11
12// Route represents a URL pattern with parameter extraction
13type Route struct {
14 Pattern string
15 Regex *regexp.Regexp
16 Params []string
17 Handler http.HandlerFunc
18}
19
20// Router manages URL routing with parameter extraction
21type Router struct {
22 routes []*Route
23}
24
25func NewRouter() *Router {
26 return &Router{routes: make([]*Route, 0)}
27}
28
29// AddRoute adds a route with parameter extraction
30// Pattern: /users/:id/posts/:postId
31func (r *Router) AddRoute(pattern string, handler http.HandlerFunc) error {
32 // Security: Validate pattern
33 if len(pattern) > 1000 {
34 return fmt.Errorf("pattern too long")
35 }
36
37 if !strings.HasPrefix(pattern, "/") {
38 return fmt.Errorf("pattern must start with /")
39 }
40
41 // Extract parameter names
42 params := make([]string, 0)
43 regexPattern := "^"
44
45 parts := strings.Split(pattern, "/")
46 for _, part := range parts {
47 if part == "" {
48 continue
49 }
50
51 if strings.HasPrefix(part, ":") {
52 paramName := part[1:]
53 if paramName == "" {
54 return fmt.Errorf("empty parameter name")
55 }
56 params = append(params, paramName)
57 regexPattern += "/([^/]+)"
58 } else {
59 regexPattern += "/" + regexp.QuoteMeta(part)
60 }
61 }
62
63 regexPattern += "$"
64
65 regex, err := regexp.Compile(regexPattern)
66 if err != nil {
67 return fmt.Errorf("invalid pattern: %w", err)
68 }
69
70 route := &Route{
71 Pattern: pattern,
72 Regex: regex,
73 Params: params,
74 Handler: handler,
75 }
76
77 r.routes = append(r.routes, route)
78 return nil
79}
80
81// Match finds a matching route and extracts parameters
82func (r *Router) Match(path string) (*Route, map[string]string, error) {
83 // Security: Path length limit
84 if len(path) > 2000 {
85 return nil, nil, fmt.Errorf("path too long")
86 }
87
88 for _, route := range r.routes {
89 matches := route.Regex.FindStringSubmatch(path)
90 if matches != nil {
91 params := make(map[string]string)
92 for i, paramName := range route.Params {
93 if i+1 < len(matches) {
94 params[paramName] = matches[i+1]
95 }
96 }
97 return route, params, nil
98 }
99 }
100
101 return nil, nil, fmt.Errorf("no route found")
102}
103
104// ServeHTTP implements http.Handler
105func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
106 route, params, err := r.Match(req.URL.Path)
107 if err != nil {
108 http.NotFound(w, req)
109 return
110 }
111
112 // Store params in request context or pass to handler
113 route.Handler(w, req)
114}
115
116func FuzzRouter(f *testing.F) {
117 // Seed corpus with various URL patterns
118 f.Add("/users")
119 f.Add("/users/123")
120 f.Add("/users/123/posts/456")
121 f.Add("/api/v1/resources")
122 f.Add("")
123 f.Add("/")
124 f.Add("//double//slash")
125 f.Add("/path/../traversal")
126 f.Add("/path/with spaces")
127 f.Add("/path%20encoded")
128 f.Add("/unicode/世界")
129 f.Add("/emoji/👍")
130
131 f.Fuzz(func(t *testing.T, path string) {
132 router := NewRouter()
133
134 // Add some routes
135 router.AddRoute("/users", func(w http.ResponseWriter, r *http.Request) {})
136 router.AddRoute("/users/:id", func(w http.ResponseWriter, r *http.Request) {})
137 router.AddRoute("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {})
138 router.AddRoute("/api/v1/resources", func(w http.ResponseWriter, r *http.Request) {})
139
140 // Should never panic on any input
141 route, params, err := router.Match(path)
142
143 if err == nil {
144 // If match succeeds, verify properties
145
146 // 1. Route should not be nil
147 if route == nil {
148 t.Error("Match succeeded but route is nil")
149 }
150
151 // 2. Params should match route parameter count
152 if len(params) != len(route.Params) {
153 t.Errorf("Param count mismatch: got %d, expected %d",
154 len(params), len(route.Params))
155 }
156
157 // 3. Each param should be present
158 for _, paramName := range route.Params {
159 if _, ok := params[paramName]; !ok {
160 t.Errorf("Missing parameter: %s", paramName)
161 }
162 }
163
164 // 4. Param values should not be empty for matched routes
165 for paramName, value := range params {
166 if value == "" {
167 t.Errorf("Empty parameter value for: %s", paramName)
168 }
169
170 // Security: Param values should not contain path traversal
171 if strings.Contains(value, "..") {
172 t.Errorf("Path traversal in param: %s", value)
173 }
174 }
175
176 // 5. Test actual HTTP request handling
177 req := httptest.NewRequest("GET", path, nil)
178 w := httptest.NewRecorder()
179
180 // Should not panic
181 router.ServeHTTP(w, req)
182
183 // Response should have valid status
184 if w.Code < 100 || w.Code > 599 {
185 t.Errorf("Invalid HTTP status: %d", w.Code)
186 }
187 }
188 })
189}
190
191func main() {
192 router := NewRouter()
193
194 // Add routes
195 router.AddRoute("/users", func(w http.ResponseWriter, r *http.Request) {
196 fmt.Fprintf(w, "List users")
197 })
198
199 router.AddRoute("/users/:id", func(w http.ResponseWriter, r *http.Request) {
200 fmt.Fprintf(w, "Get user")
201 })
202
203 router.AddRoute("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
204 fmt.Fprintf(w, "Get post")
205 })
206
207 // Test matching
208 testPaths := []string{
209 "/users",
210 "/users/123",
211 "/users/123/posts/456",
212 "/invalid",
213 "/users/123/posts",
214 }
215
216 fmt.Println("=== Route Matching Tests ===")
217 for _, path := range testPaths {
218 route, params, err := router.Match(path)
219 if err != nil {
220 fmt.Printf("%-30s -> No match\n", path)
221 } else {
222 fmt.Printf("%-30s -> Pattern: %s, Params: %v\n",
223 path, route.Pattern, params)
224 }
225 }
226}
Exercise 4: State Machine Fuzzer
Create a state machine with transitions and use fuzzing to find invalid state transitions or deadlocks.
Solution
1package main
2
3import (
4 "fmt"
5 "sync"
6 "testing"
7)
8
9// State represents a state in the machine
10type State int
11
12const (
13 StateInit State = iota
14 StateRunning
15 StatePaused
16 StateStopped
17 StateError
18)
19
20func (s State) String() string {
21 switch s {
22 case StateInit:
23 return "Init"
24 case StateRunning:
25 return "Running"
26 case StatePaused:
27 return "Paused"
28 case StateStopped:
29 return "Stopped"
30 case StateError:
31 return "Error"
32 default:
33 return "Unknown"
34 }
35}
36
37// Transition represents a state transition
38type Transition int
39
40const (
41 TransitionStart Transition = iota
42 TransitionPause
43 TransitionResume
44 TransitionStop
45 TransitionFail
46 TransitionReset
47)
48
49func (t Transition) String() string {
50 switch t {
51 case TransitionStart:
52 return "Start"
53 case TransitionPause:
54 return "Pause"
55 case TransitionResume:
56 return "Resume"
57 case TransitionStop:
58 return "Stop"
59 case TransitionFail:
60 return "Fail"
61 case TransitionReset:
62 return "Reset"
63 default:
64 return "Unknown"
65 }
66}
67
68// StateMachine manages state transitions
69type StateMachine struct {
70 current State
71 mu sync.RWMutex
72 history []State
73}
74
75func NewStateMachine() *StateMachine {
76 return &StateMachine{
77 current: StateInit,
78 history: []State{StateInit},
79 }
80}
81
82// GetState returns current state
83func (sm *StateMachine) GetState() State {
84 sm.mu.RLock()
85 defer sm.mu.RUnlock()
86 return sm.current
87}
88
89// Transition attempts a state transition
90func (sm *StateMachine) Transition(t Transition) error {
91 sm.mu.Lock()
92 defer sm.mu.Unlock()
93
94 oldState := sm.current
95 newState, err := sm.getNextState(sm.current, t)
96 if err != nil {
97 return err
98 }
99
100 sm.current = newState
101 sm.history = append(sm.history, newState)
102
103 return nil
104}
105
106// getNextState determines next state based on current state and transition
107func (sm *StateMachine) getNextState(current State, t Transition) (State, error) {
108 switch current {
109 case StateInit:
110 switch t {
111 case TransitionStart:
112 return StateRunning, nil
113 case TransitionFail:
114 return StateError, nil
115 default:
116 return current, fmt.Errorf("invalid transition %s from %s", t, current)
117 }
118
119 case StateRunning:
120 switch t {
121 case TransitionPause:
122 return StatePaused, nil
123 case TransitionStop:
124 return StateStopped, nil
125 case TransitionFail:
126 return StateError, nil
127 default:
128 return current, fmt.Errorf("invalid transition %s from %s", t, current)
129 }
130
131 case StatePaused:
132 switch t {
133 case TransitionResume:
134 return StateRunning, nil
135 case TransitionStop:
136 return StateStopped, nil
137 case TransitionFail:
138 return StateError, nil
139 default:
140 return current, fmt.Errorf("invalid transition %s from %s", t, current)
141 }
142
143 case StateStopped:
144 switch t {
145 case TransitionReset:
146 return StateInit, nil
147 default:
148 return current, fmt.Errorf("invalid transition %s from %s", t, current)
149 }
150
151 case StateError:
152 switch t {
153 case TransitionReset:
154 return StateInit, nil
155 default:
156 return current, fmt.Errorf("invalid transition %s from %s", t, current)
157 }
158
159 default:
160 return current, fmt.Errorf("unknown state: %s", current)
161 }
162}
163
164// GetHistory returns state transition history
165func (sm *StateMachine) GetHistory() []State {
166 sm.mu.RLock()
167 defer sm.mu.RUnlock()
168 return append([]State{}, sm.history...)
169}
170
171func FuzzStateMachine(f *testing.F) {
172 // Seed with valid transition sequences
173 f.Add([]byte{0, 1, 2, 3}) // Start, Pause, Resume, Stop
174 f.Add([]byte{0, 3}) // Start, Stop
175 f.Add([]byte{4}) // Fail
176 f.Add([]byte{}) // Empty sequence
177
178 f.Fuzz(func(t *testing.T, transitionBytes []byte) {
179 // Skip extremely long sequences
180 if len(transitionBytes) > 100 {
181 return
182 }
183
184 sm := NewStateMachine()
185
186 // Track states visited
187 statesVisited := make(map[State]bool)
188 statesVisited[sm.GetState()] = true
189
190 // Apply transitions
191 for _, b := range transitionBytes {
192 // Map byte to valid transition
193 transition := Transition(int(b) % 6)
194
195 oldState := sm.GetState()
196 err := sm.Transition(transition)
197
198 newState := sm.GetState()
199 statesVisited[newState] = true
200
201 // Verify invariants
202 if err == nil {
203 // 1. State should have changed for valid transition
204 if newState == oldState && transition != TransitionStart {
205 // Some transitions might keep same state, that's ok
206 }
207
208 // 2. New state should be valid
209 if newState < StateInit || newState > StateError {
210 t.Errorf("Invalid state after transition: %d", newState)
211 }
212
213 // 3. History should have grown
214 history := sm.GetHistory()
215 if len(history) == 0 {
216 t.Error("History is empty after transitions")
217 }
218
219 // 4. Last history entry should match current state
220 if history[len(history)-1] != newState {
221 t.Errorf("History mismatch: last=%s, current=%s",
222 history[len(history)-1], newState)
223 }
224 }
225 }
226
227 // Verify final state is valid
228 finalState := sm.GetState()
229 if finalState < StateInit || finalState > StateError {
230 t.Errorf("Invalid final state: %d", finalState)
231 }
232
233 // Verify we can always reach Init via Reset
234 if finalState == StateStopped || finalState == StateError {
235 err := sm.Transition(TransitionReset)
236 if err != nil {
237 t.Errorf("Cannot reset from terminal state %s: %v", finalState, err)
238 }
239
240 if sm.GetState() != StateInit {
241 t.Errorf("Reset did not return to Init: got %s", sm.GetState())
242 }
243 }
244 })
245}
246
247func main() {
248 sm := NewStateMachine()
249
250 fmt.Println("=== State Machine Demo ===")
251 fmt.Printf("Initial state: %s\n\n", sm.GetState())
252
253 // Valid transition sequence
254 transitions := []Transition{
255 TransitionStart,
256 TransitionPause,
257 TransitionResume,
258 TransitionStop,
259 TransitionReset,
260 }
261
262 for _, t := range transitions {
263 oldState := sm.GetState()
264 err := sm.Transition(t)
265
266 fmt.Printf("%s: %s", t, oldState)
267 if err != nil {
268 fmt.Printf(" -> ERROR: %v\n", err)
269 } else {
270 fmt.Printf(" -> %s ✓\n", sm.GetState())
271 }
272 }
273
274 fmt.Println("\n=== State History ===")
275 for i, state := range sm.GetHistory() {
276 fmt.Printf("%d. %s\n", i+1, state)
277 }
278}
Exercise 5: Compression Fuzzer
Build a simple compression/decompression system and fuzz it to ensure round-trip correctness and handle malformed data.
Solution
1package main
2
3import (
4 "bytes"
5 "compress/gzip"
6 "fmt"
7 "io"
8 "testing"
9)
10
11// Compressor handles data compression with safety limits
12type Compressor struct {
13 maxInputSize int
14 maxOutputSize int
15}
16
17func NewCompressor() *Compressor {
18 return &Compressor{
19 maxInputSize: 10 * 1024 * 1024, // 10MB
20 maxOutputSize: 100 * 1024 * 1024, // 100MB
21 }
22}
23
24// Compress compresses data with safety checks
25func (c *Compressor) Compress(data []byte) ([]byte, error) {
26 // Security: Input size limit
27 if len(data) > c.maxInputSize {
28 return nil, fmt.Errorf("input too large: %d bytes", len(data))
29 }
30
31 var buf bytes.Buffer
32 writer := gzip.NewWriter(&buf)
33
34 _, err := writer.Write(data)
35 if err != nil {
36 return nil, fmt.Errorf("compression failed: %w", err)
37 }
38
39 if err := writer.Close(); err != nil {
40 return nil, fmt.Errorf("compression finalize failed: %w", err)
41 }
42
43 compressed := buf.Bytes()
44
45 // Security: Output size check
46 if len(compressed) > c.maxOutputSize {
47 return nil, fmt.Errorf("compressed data too large: %d bytes", len(compressed))
48 }
49
50 return compressed, nil
51}
52
53// Decompress decompresses data with safety checks
54func (c *Compressor) Decompress(data []byte) ([]byte, error) {
55 // Security: Input size limit
56 if len(data) > c.maxInputSize {
57 return nil, fmt.Errorf("compressed data too large: %d bytes", len(data))
58 }
59
60 reader, err := gzip.NewReader(bytes.NewReader(data))
61 if err != nil {
62 return nil, fmt.Errorf("decompression init failed: %w", err)
63 }
64 defer reader.Close()
65
66 // Use limited reader to prevent decompression bombs
67 limitedReader := io.LimitReader(reader, int64(c.maxOutputSize))
68
69 var buf bytes.Buffer
70 _, err = io.Copy(&buf, limitedReader)
71 if err != nil {
72 return nil, fmt.Errorf("decompression failed: %w", err)
73 }
74
75 decompressed := buf.Bytes()
76
77 // Check if we hit the limit (decompression bomb)
78 if len(decompressed) == c.maxOutputSize {
79 return nil, fmt.Errorf("decompressed data exceeds size limit")
80 }
81
82 return decompressed, nil
83}
84
85func FuzzCompressor(f *testing.F) {
86 // Seed corpus with various data patterns
87 f.Add([]byte("hello world"))
88 f.Add([]byte(""))
89 f.Add([]byte("a"))
90 f.Add(bytes.Repeat([]byte("A"), 1000)) // Highly compressible
91 f.Add([]byte{0, 1, 2, 3, 4, 5, 6, 7}) // Binary data
92
93 // Random-looking data (low compressibility)
94 random := make([]byte, 100)
95 for i := range random {
96 random[i] = byte(i * 7 % 256)
97 }
98 f.Add(random)
99
100 f.Fuzz(func(t *testing.T, data []byte) {
101 // Skip very large inputs to keep fuzzing fast
102 if len(data) > 100000 {
103 return
104 }
105
106 compressor := NewCompressor()
107
108 // Compress
109 compressed, compressErr := compressor.Compress(data)
110
111 if compressErr == nil {
112 // Property 1: Compressed data should not be too large
113 if len(compressed) > len(data)*2+1000 {
114 t.Errorf("Compression inefficient: %d -> %d bytes",
115 len(data), len(compressed))
116 }
117
118 // Property 2: Should be able to decompress
119 decompressed, decompressErr := compressor.Decompress(compressed)
120
121 if decompressErr != nil {
122 t.Errorf("Decompression failed after successful compression: %v",
123 decompressErr)
124 } else {
125 // Property 3: Round-trip should preserve data
126 if !bytes.Equal(data, decompressed) {
127 t.Errorf("Round-trip failed: original %d bytes, got %d bytes",
128 len(data), len(decompressed))
129 }
130
131 // Property 4: Double compression should work
132 compressed2, err := compressor.Compress(decompressed)
133 if err != nil {
134 t.Errorf("Second compression failed: %v", err)
135 }
136
137 // Property 5: Double decompression should work
138 if compressed2 != nil {
139 decompressed2, err := compressor.Decompress(compressed2)
140 if err != nil {
141 t.Errorf("Second decompression failed: %v", err)
142 }
143
144 if !bytes.Equal(data, decompressed2) {
145 t.Error("Double round-trip failed")
146 }
147 }
148 }
149 }
150
151 // Test decompression of arbitrary data (should handle gracefully)
152 _, decompressErr := compressor.Decompress(data)
153 // It's OK if this fails, just shouldn't panic
154 _ = decompressErr
155 })
156}
157
158func main() {
159 compressor := NewCompressor()
160
161 testCases := []struct {
162 name string
163 data []byte
164 }{
165 {"Empty", []byte{}},
166 {"Small text", []byte("hello world")},
167 {"Repeated", bytes.Repeat([]byte("A"), 1000)},
168 {"Binary", []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
169 }
170
171 fmt.Println("=== Compression Tests ===")
172 for _, tc := range testCases {
173 compressed, err := compressor.Compress(tc.data)
174 if err != nil {
175 fmt.Printf("%-20s: Compression failed: %v\n", tc.name, err)
176 continue
177 }
178
179 decompressed, err := compressor.Decompress(compressed)
180 if err != nil {
181 fmt.Printf("%-20s: Decompression failed: %v\n", tc.name, err)
182 continue
183 }
184
185 ratio := float64(len(compressed)) / float64(len(tc.data))
186 if len(tc.data) == 0 {
187 ratio = 0
188 }
189
190 match := "✓"
191 if !bytes.Equal(tc.data, decompressed) {
192 match = "✗"
193 }
194
195 fmt.Printf("%-20s: %d -> %d bytes (%.2f%%) %s\n",
196 tc.name, len(tc.data), len(compressed), ratio*100, match)
197 }
198}
Further Reading
Official Documentation
- Go Fuzzing Tutorial - Official Go fuzzing guide
- Fuzzing Design Draft - Technical details
- testing.F Documentation - API reference
Books and Papers
- The Fuzzing Book - Comprehensive guide to fuzzing techniques
- Fuzzing: Brute Force Vulnerability Discovery - Security-focused approach
- Fuzzing Research Papers - Academic research
Tools and Services
- OSS-Fuzz - Free continuous fuzzing for open source
- AFL - Advanced fuzzy testing
- LibFuzzer - LLVM's fuzzing engine
Security Resources
- CVE Database - Learn from past vulnerabilities
- OWASP Testing Guide - Web security testing
- Google Bug Bounties - Real-world vulnerability examples
Summary
Key Takeaways
- Fuzzing discovers bugs unit tests miss by exploring unexpected inputs
- Property-based testing is more effective than testing specific outcomes
- Go's native fuzzing makes comprehensive testing accessible
- Security-focused fuzzing prevents production vulnerabilities
- Integration with CI/CD provides continuous protection
When to Use Fuzz Testing
✅ Perfect for:
- Input parsers (JSON, XML, binary formats)
- String processing and template engines
- Network protocol handlers
- Authentication and validation logic
- Serialization/deserialization code
- Regular expression engines
- State machines and workflow systems
⚠️ Less effective for:
- Pure mathematical computations
- Simple CRUD database operations
- Configuration-only code
- External API integration tests
Mastery Path
Beginner: Write basic fuzz tests for simple functions
Intermediate: Implement property-based testing with invariants
Advanced: Create complex fuzz targets for parsers and protocols
Expert: Integrate continuous fuzzing with corpus management
Next Steps:
- Add fuzz tests to your current project
- Set up automated fuzzing in CI/CD
- Learn from OSS-Fuzz success stories
- Explore advanced fuzzing techniques
Remember: A single fuzz test that finds one critical vulnerability is worth more than a hundred unit tests that verify expected behavior.