Day 87 — Fuzz Testing
Learning Objectives
- Write fuzz tests for WAF normalization and JSON schema validator
- Find crashes and panics with
go test -fuzz - Fix all panics before deploying security-critical code
Exercise: Fuzz the WAF
// fuzz_test.go
package waf_test
import (
"testing"
"unicode/utf8"
)
// FuzzNormalizationPipeline finds inputs that panic the normalizer
func FuzzNormalizationPipeline(f *testing.F) {
// Seed corpus — known interesting inputs
seeds := []string{
"",
"\x00",
"%",
"%%",
"%2e%2e%2f",
"%252e%252e%252f",
"' OR '1'='1",
"<script>alert(1)</script>",
strings.Repeat("A", 10000), // very long input
"\xff\xfe", // invalid UTF-8
"�", // null HTML entity
"javascript:%0a%0dalert(1)", // newline-encoded JS
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, input string) {
// Must not panic on any input
result := NormalizationPipeline(input)
// Must return valid UTF-8 (so downstream string operations are safe)
if !utf8.ValidString(result) {
t.Errorf("normalization returned invalid UTF-8 for input %q", input)
}
// Must not return longer than input (normalization should not expand)
if len(result) > len(input)*10 {
t.Errorf("normalization expanded input suspiciously: %d → %d", len(input), len(result))
}
})
}
// FuzzJSONSchemaValidator finds inputs that panic the schema validator
func FuzzJSONSchemaValidator(f *testing.F) {
seeds := [][]byte{
[]byte(`{}`),
[]byte(`{"username":"alice","email":"a@b.com","role":"admin","password":"secret123"}`),
[]byte(`null`),
[]byte(`[]`),
[]byte(`{{{`), // malformed JSON
[]byte(`{"x":` + strings.Repeat("[", 1000)), // deeply nested
}
for _, s := range seeds {
f.Add(s)
}
schema := &Schema{
Type: "object",
Required: []string{"username", "email"},
Properties: map[string]*Schema{
"username": {Type: "string", MinLength: ptr(3), MaxLength: ptr(64)},
"email": {Type: "string"},
},
}
f.Fuzz(func(t *testing.T, body []byte) {
// Must not panic
var data interface{}
json.Unmarshal(body, &data)
_ = schema.Validate(data, "$")
})
}
Run the fuzzer:
# Fuzz for 30 seconds
go test -fuzz=FuzzNormalizationPipeline -fuzztime=30s ./...
go test -fuzz=FuzzJSONSchemaValidator -fuzztime=30s ./...
# If a crash is found, it's saved to testdata/fuzz/
# Run to reproduce:
go test -run=FuzzNormalizationPipeline/testdata/fuzz/...
Real-World Context
Go's built-in fuzzer finds bugs in security code that hand-written tests miss. In 2022, fuzzing found ~30 vulnerabilities in the Go standard library. For security-critical code (WAF, parsers, crypto), fuzzing for 10 minutes before each release is best practice. The testdata/fuzz/ directory stores crash-inducing inputs as regression tests.