Fuzzing a Go REST API Handler
Crash course in HTTP handler fuzzing

Software Engineer x Data Engineer - I make the world a better place to live with software that enables data-driven decision-making
Fuzzing
Fuzzing is a type of automated testing which continuously manipulates inputs to a program to find bugs. Go fuzzing uses coverage guidance to intelligently walk through the code being fuzzed to find and report failures to the user. Since it can reach edge cases which humans often miss, fuzz testing can be particularly valuable for finding security exploits and vulnerabilities.
Mostly fuzzing is being used in parsers, formatters, decoders, network handlers and other security-critical code.
It helps to find:
panics
nil pointer dereferences
memleaks
unhandled errors
unexpected behaviors
security vulnerabilities (like buffer overflow or infinite loops)
But hey, what about the web API’s? The REST endpoints are full of user input handling and this is the perfect place for fuzzing!
The Handler
Let’s assume we have an endpoint responsible for a user creation. The input payload comes in the JSON format:
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Age int `json:"age"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if req.Age < 0 {
http.Error(w, "Invalid age", http.StatusBadRequest)
return
}
// Create user logic goes here.
fmt.Fprintf(w, "User %s created successfully", req.Username)
}
The happy path is very simple, we decode the input payload into the struct, run some validation and register a user in the system.
As you can see there’s nothing special, but the JSON encoding part might get tricky. The JSON payload might be malformed, might cause unexpected results in further processing.
Would be nice to have something extra that will cover our handler with extra tests.
The Fuzz Test
The fuzz test in its basic form is an abstract layer over the regular *testing.T framework - we’re going to use the *testing.F input param.
There are couple of requirements for the fuzz test to work, like the name has to follow the pattern FuzzName, you can read more on it here.
Let’s focus on our basic fuzz test:
func FuzzCreateUserHandler(f *testing.F) {
// Seed corpus with a valid JSON input
f.Add(`{"username":"testuser","age":30}`)
f.Fuzz(func(t *testing.T, body string) {
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateUserHandler(w, req)
if w.Result().StatusCode == http.StatusInternalServerError {
t.Errorf("Unexpected internal server error for input: %q", body)
}
})
}
What’s happening in our test:
We’re feeding our seed corpus with some valid test cases
Go’s fuzzer will mutate the input: remove/replace fields, add random bytes, change numbers, etc.
We’re asserting that if the result’s
StatusCodeishttp.StatusInternalServerErrorthen the test should fail
The more sophisticated fuzz test, that will check for panic would be:
func FuzzCreateUserHandler(f *testing.F) {
// Seed corpus with a valid JSON input
f.Add(`{"username":"testuser","age":30}`)
f.Fuzz(func(t *testing.T, body string) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("Handler panicked with input %q: %v", body, r)
}
}()
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
CreateUserHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
if resp.StatusCode == http.StatusInternalServerError {
t.Errorf("Unexpected 500 Internal Server Error for input: %q", body)
}
})
}
As you can see we’re using the recover() function that is checked at the end of the function call to see if there were any unexpected panics.
What you will assert is only up to you, the sky is the limit.
Run Fuzz Test
Okay, but how to run this test?
It’s simple, the regular tests you run go test ./… - the same way you can run the fuzz test btw (but limited to the regular execution - without fuzzing).
The fuzz test, you run:
go test -fuzz=Fuzz -fuzztime 10s
Please note the -fuzztime param. The fuzz tests can run really long, if you want to limit its execution time, this is the way.
The typical fuzz test output is similar to this:
go test -fuzz=Fuzz -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/411 completed
fuzz: elapsed: 0s, gathering baseline coverage: 411/411 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 244934 (81632/sec), new interesting: 0 (total: 411)
fuzz: elapsed: 6s, execs: 513074 (89363/sec), new interesting: 3 (total: 414)
fuzz: elapsed: 9s, execs: 785859 (90958/sec), new interesting: 4 (total: 415)
fuzz: elapsed: 10s, execs: 882683 (89005/sec), new interesting: 5 (total: 416)
PASS
ok github.com/flashlabs/kiss-samples/fuzzing 10.445s
What does those sections even mean?
gathering baseline coverage- before Go starts throwing random data at your code, it first runs your fuzz target with all the seeded inputs in your corpus.gathering baseline coverage: 0/411 completed- it means that Go has prepared a 411 inputs and run 0 out of itgathering baseline coverage: 411/411 completed- now it’s done. Go knows what your code coverage looks like before fuzzingnow fuzzing with 8 workers- Go is launching 8 parallel processes in the background (8 goroutines) that will now work on ourinput, mutate it and feed it to our test function, looking forcrashes
panics
failed assertions
new interesting: 3 (total: 414)- Go has found 3 new inputs that increase the code coverage, total corpus is now 414 (411 + 3)- keep in mind that since those input doesn’t trigger the
t.Fatalf()ort.Errorf()and there’s no panic they’re considered safe and a run is considered a successfull one
- keep in mind that since those input doesn’t trigger the
The logic for the new inputs and the code coverage:
┌────────────────────────────────────────────┐
│ New input found │
└────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Does it increase coverage? │───► YES ──► Corpus grows
└────────────────────────────┘
│
▼
NO │
▼
Input is discarded
Not every interesting input is a bug — fuzzers also collect inputs that increase code coverage. These inputs expand the test corpus, helping the fuzzer explore more paths over time.
Summary
While fuzzing is often associated with lower-level data parsing, it’s a powerful tool for web APIs too — especially since every API endpoint is essentially a parser of user-controlled input.
You need to experiment locally with different assertions and fuzzing times.
The full source code for this example with the http server you can find on the https://github.com/flashlabs/kiss-samples/tree/main/fuzzing




