Day 117 — Fuzz Testing Critical Parsers
| Learning objective | Write production-grade fuzz tests for WAF normalization, JA3 parser, and binary protocol parser |
| Key packages | testing/fuzz |
| Security context | A panic in your WAF is a denial-of-service — fuzz testing is the most effective way to find panic paths |
Key Concepts
What to fuzz:
- Any code that parses input of arbitrary length/content
- Any code with complex branching on input content (WAF rules, protocol parsers)
- Any code where wrong behavior has security impact (auth, schema validator)
Exercise — Fuzz Tests
package fuzz_test
import (
"testing"
// Import your packages
)
// FuzzNormalizationPipeline — does the WAF normalizer panic on arbitrary input?
func FuzzNormalizationPipeline(f *testing.F) {
// Seed corpus — known interesting inputs
seeds := []string{
"",
"normal input",
"%27 OR 1=1--",
"<script>alert(1)</script>",
"../../../../etc/passwd",
string([]byte{0x00, 0x01, 0x02}), // null bytes
string([]byte{0xff, 0xfe}), // invalid UTF-8
"%gg%hh", // invalid percent encoding
"%%2527", // double-double encoded
"a" + string(make([]byte, 10000)), // very long input
"a\r\n\r\nGET / HTTP/1.1", // CRLF injection
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, input string) {
// Must not panic on any input
result := normalize(input)
// Result must be a valid string (non-nil, valid UTF-8 is not required)
_ = result
// Normalization must be idempotent: normalize(normalize(x)) == normalize(x)
if normalize(result) != result {
t.Errorf("normalization not idempotent for input %q: first=%q second=%q",
input, result, normalize(result))
}
})
}
// FuzzJSONSchemaValidator — does malformed JSON panic the schema validator?
func FuzzJSONSchemaValidator(f *testing.F) {
seeds := []string{
`{}`,
`{"name":"test"}`,
`{"name":null}`,
`{"name":true}`,
`[1,2,3]`,
`{"a":{"b":{"c":{"d":{}}}}}`, // deep nesting
`{"\u0000":"value"}`, // null byte in key
`{"key":"` + string(make([]byte, 100000)) + `"}`, // huge value
`{`, // truncated
`{"k":`, // truncated
`{} extra`, // trailing garbage
}
for _, s := range seeds {
f.Add(s)
}
schema := &JSONSchema{
Type: "object",
Required: []string{"name"},
Properties: map[string]*JSONSchema{"name": {Type: "string", MaxLength: 255}},
AdditionalProperties: boolPtr(false),
}
f.Fuzz(func(t *testing.T, input string) {
// Must not panic
errs := schema.Validate([]byte(input))
// Just check it returns without panicking
_ = errs
})
}
// FuzzBinaryProtocolParser — does the binary parser panic on arbitrary bytes?
func FuzzBinaryProtocolParser(f *testing.F) {
seeds := [][]byte{
{},
{0x00, 0x00, 0x00, 0x00}, // zero-length packet
{0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o'}, // valid packet
{0xff, 0xff, 0xff, 0xff}, // max length
{0x00, 0x00, 0x00, 0x01}, // length 1, no data
make([]byte, 1024), // all zeros
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, data []byte) {
// Must not panic — parsePacket must return an error for invalid input
payload, err := parsePacket(data)
if err != nil {
return // expected for most random input
}
// If no error: payload must be a valid slice
if payload == nil {
t.Error("parsePacket returned nil payload with nil error")
}
// Round-trip test: re-parsing the encoded payload should give same result
// (only if your protocol has a serialization function)
})
}
// Run fuzz tests:
// go test -fuzz=FuzzNormalizationPipeline -fuzztime=60s ./internal/waf/...
// go test -fuzz=FuzzJSONSchemaValidator -fuzztime=60s ./internal/validation/...
// go test -fuzz=FuzzBinaryProtocolParser -fuzztime=60s ./internal/protocol/...