Files
goyco/internal/fuzz/fuzz.go

227 lines
5.4 KiB
Go

package fuzz
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"unicode/utf8"
)
type FuzzTestHelper struct{}
func NewFuzzTestHelper() *FuzzTestHelper {
return &FuzzTestHelper{}
}
func (h *FuzzTestHelper) RunBasicFuzzTest(f *testing.F, testFunc func(t *testing.T, input string)) {
f.Add("test input")
f.Fuzz(func(t *testing.T, input string) {
if !utf8.ValidString(input) {
return
}
testFunc(t, input)
})
}
func (h *FuzzTestHelper) RunValidationFuzzTest(f *testing.F, validateFunc func(string) error) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
err := validateFunc(input)
_ = err
})
}
func (h *FuzzTestHelper) RunSanitizationFuzzTest(f *testing.F, sanitizeFunc func(string) string) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
result := sanitizeFunc(input)
if !utf8.ValidString(result) {
t.Fatal("Sanitized result contains invalid UTF-8")
}
})
}
func (h *FuzzTestHelper) RunSanitizationFuzzTestWithValidation(f *testing.F, sanitizeFunc func(string) string, validateFunc func(string) bool) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
result := sanitizeFunc(input)
if !utf8.ValidString(result) {
t.Fatal("Sanitized result contains invalid UTF-8")
}
if validateFunc != nil {
if !validateFunc(result) {
t.Fatal("Sanitized result failed validation")
}
}
})
}
func (h *FuzzTestHelper) RunJSONFuzzTest(f *testing.F, testCases []map[string]any) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
for _, tc := range testCases {
body, ok := tc["body"].(string)
if !ok {
continue
}
encoded, err := json.Marshal(input)
if err != nil {
return
}
encodedStr := string(encoded)
body = strings.ReplaceAll(body, "FUZZED_INPUT", encodedStr)
var result map[string]any
err = json.Unmarshal([]byte(body), &result)
if err != nil {
return
}
}
})
}
func (h *FuzzTestHelper) RunHTTPFuzzTest(f *testing.F, testCases []HTTPFuzzTestCase) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
for _, tc := range testCases {
sanitized := h.sanitizeForURL(input)
url := strings.ReplaceAll(tc.URL, "FUZZED_INPUT", sanitized)
body := strings.ReplaceAll(tc.Body, "FUZZED_INPUT", sanitized)
req := httptest.NewRequest(tc.Method, url, bytes.NewBufferString(body))
for name, value := range tc.Headers {
req.Header.Set(name, value)
}
h.validateHTTPRequest(t, req)
}
})
}
func (h *FuzzTestHelper) sanitizeForURL(input string) string {
sanitized := strings.ReplaceAll(input, "\n", "")
sanitized = strings.ReplaceAll(sanitized, "\r", "")
sanitized = strings.ReplaceAll(sanitized, "\t", "")
sanitized = url.QueryEscape(sanitized)
sanitized = strings.ReplaceAll(sanitized, "+", "%20")
if len(sanitized) > 100 {
sanitized = sanitized[:100]
}
return sanitized
}
type HTTPFuzzTestCase struct {
Name string
Method string
URL string
Headers map[string]string
Body string
}
func (h *FuzzTestHelper) validateHTTPRequest(t *testing.T, req *http.Request) {
pathParts := strings.Split(req.URL.Path, "/")
for _, part := range pathParts {
if !utf8.ValidString(part) {
t.Fatal("Path contains invalid UTF-8")
}
}
for name, values := range req.URL.Query() {
if !utf8.ValidString(name) {
t.Fatal("Query parameter name contains invalid UTF-8")
}
for _, value := range values {
if !utf8.ValidString(value) {
t.Fatal("Query parameter value contains invalid UTF-8")
}
}
}
for name, values := range req.Header {
if !utf8.ValidString(name) {
t.Fatal("Header name contains invalid UTF-8")
}
for _, value := range values {
if !utf8.ValidString(value) {
t.Fatal("Header value contains invalid UTF-8")
}
}
}
}
func (h *FuzzTestHelper) RunIntegrationFuzzTest(f *testing.F, testFunc func(t *testing.T, input string)) {
h.RunBasicFuzzTest(f, func(t *testing.T, input string) {
if len(input) > 1000 {
input = input[:1000]
}
testFunc(t, input)
})
}
func (h *FuzzTestHelper) GetCommonAuthTestCases(input string) []HTTPFuzzTestCase {
return []HTTPFuzzTestCase{
{
Name: "auth_register",
Method: "POST",
URL: "/api/auth/register",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: `{"username":"FUZZED_INPUT","email":"test@example.com","password":"test123"}`,
},
{
Name: "auth_login",
Method: "POST",
URL: "/api/auth/login",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: `{"username":"FUZZED_INPUT","password":"test123"}`,
},
}
}
func (h *FuzzTestHelper) GetCommonPostTestCases(input string) []HTTPFuzzTestCase {
return []HTTPFuzzTestCase{
{
Name: "post_create",
Method: "POST",
URL: "/api/posts",
Headers: map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer FUZZED_INPUT",
},
Body: `{"title":"FUZZED_INPUT","url":"https://example.com","content":"test"}`,
},
{
Name: "post_search",
Method: "GET",
URL: "/api/posts/search?q=FUZZED_INPUT",
Headers: map[string]string{
"Content-Type": "application/json",
},
},
}
}
func (h *FuzzTestHelper) GetCommonVoteTestCases(input string) []HTTPFuzzTestCase {
return []HTTPFuzzTestCase{
{
Name: "vote_cast",
Method: "POST",
URL: "/api/posts/1/vote",
Headers: map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer FUZZED_INPUT",
},
Body: `{"type":"FUZZED_INPUT"}`,
},
}
}