-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
engineering: go commentary in nov 01
- Loading branch information
Showing
1 changed file
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
--- | ||
tags: | ||
- golang | ||
- go-weekly | ||
authors: | ||
- fuatto | ||
title: 'Go Commentary #18: Fuzz Testing Go HTTP Services' | ||
short_title: '#18 Fuzz Testing Go HTTP Services' | ||
description: Understanding how to use Fuzz Testing in Go | ||
date: 2024-11-01 | ||
--- | ||
|
||
## [Fuzz Testing Go HTTP Services](https://packagemain.tech/p/fuzzing-http-services-golang) | ||
|
||
Context: | ||
|
||
- You can't envision all of the possible inputs your code could receive => You can only find bugs that you expect to find | ||
|
||
Solution: | ||
|
||
- Since Go 1.18, fuzzing was added to Go's std testing package | ||
|
||
``` | ||
The steps to create a fuzz test in Go are the following: | ||
In a _test.go file create a function that starts with Fuzz which accepts *testing.F | ||
Add corpus seeds using f.Add() to allow fuzzer to generate the data based on it. | ||
Call fuzz target using f.Fuzz() by passing fuzzing arguments which our target function accepts. | ||
Start the fuzzer using regular go test command, but with the –fuzz=Fuzz flag | ||
``` | ||
|
||
- Example: | ||
|
||
```go | ||
func Equal(a []byte, b []byte) bool { | ||
for i := range a { | ||
// can panic with runtime error: index out of range. | ||
if a[i] != b[i] { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
``` | ||
|
||
```go | ||
// Fuzz test | ||
func FuzzEqual(f *testing.F) { | ||
|
||
// Seed corpus addition | ||
f.Add([]byte{'f', 'u', 'z', 'z'}, []byte{'t', 'e', 's', 't'}) | ||
|
||
// Fuzz target with fuzzing arguments | ||
f.Fuzz(func(t *testing.T, a []byte, b []byte) { | ||
// Call our target function and pass fuzzing arguments | ||
Equal(a, b) | ||
}) | ||
} | ||
``` | ||
|
||
- Fuzzing HTTP Services | ||
|
||
```go | ||
type Request struct { | ||
Limit int `json:"limit"` | ||
Offset int `json:"offset"` | ||
} | ||
|
||
type Response struct { | ||
Results []int `json:"items"` | ||
PagesCount int `json:"pagesCount"` | ||
} | ||
``` | ||
|
||
```go | ||
func ProcessRequest(w http.ResponseWriter, r *http.Request) { | ||
var req Request | ||
|
||
// Decode JSON request | ||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
http.Error(w, err.Error(), http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// Apply offset and limit to some static data | ||
all := make([]int, 1000) | ||
start := req.Offset | ||
end := req.Offset + req.Limit | ||
res := Response{ | ||
Results: all[start:end], | ||
PagesCount: len(all) / req.Limit, | ||
} | ||
|
||
// Send JSON response | ||
if err := json.NewEncoder(w).Encode(res); err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
w.WriteHeader(http.StatusOK) | ||
} | ||
``` | ||
|
||
```go | ||
func FuzzProcessRequest(f *testing.F) { | ||
// Create sample inputs for the fuzzer | ||
testRequests := []Request{ | ||
{Limit: -10, Offset: -10}, | ||
{Limit: 0, Offset: 0}, | ||
{Limit: 100, Offset: 100}, | ||
{Limit: 200, Offset: 200}, | ||
} | ||
|
||
// Add to the seed corpus | ||
for _, r := range testRequests { | ||
if data, err := json.Marshal(r); err == nil { | ||
f.Add(data) | ||
} | ||
} | ||
|
||
// Create a test server | ||
srv := httptest.NewServer(http.HandlerFunc(ProcessRequest)) | ||
defer srv.Close() | ||
|
||
// Fuzz target with a single []byte argument | ||
f.Fuzz(func(t *testing.T, data []byte) { | ||
var req Request | ||
if err := json.Unmarshal(data, &req); err != nil { | ||
// Skip invalid JSON requests that may be generated during fuzz | ||
t.Skip("invalid json") | ||
} | ||
|
||
// Pass data to the server | ||
resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data)) | ||
if err != nil { | ||
t.Fatalf("unable to call server: %v, data: %s", err, string(data)) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
// Skip BadRequest errors | ||
if resp.StatusCode == http.StatusBadRequest { | ||
t.Skip("invalid json") | ||
} | ||
|
||
// Check status code | ||
if resp.StatusCode != http.StatusOK { | ||
t.Fatalf("non-200 status code %d", resp.StatusCode) | ||
} | ||
}) | ||
} | ||
``` | ||
|
||
``` | ||
go test --fuzz=Fuzz -fuzztime=30s | ||
--- FAIL: FuzzProcessRequest (0.02s) | ||
--- FAIL: FuzzProcessRequest (0.00s) | ||
runtime error: integer divide by zero | ||
runtime error: slice bounds out of range | ||
``` | ||
|
||
Conclusion: | ||
|
||
- Can detect hard-to-spot bugs with weird unexpected inputs | ||
|
||
|
||
--- | ||
|
||
https://packagemain.tech/p/fuzzing-http-services-golang | ||
|
||
|