close up shot of keyboard buttons

Como fazer fuzz test em requests HTTP (parte 2)

Na primeira parte desse post, vimos como utilizar o fuzz test para gerar payloads automaticamente em nossos testes, o que nos ajudou a encontrar problemas quando o payload não vinha no formato que esperávamos.

Nessa segunda parte, vamos utilizar o fuzz para gerar os inputs que serão utilizados nos campos de um payload correto.

Utilizando o mesmo código do post anterior, antes de começar, vamos fazer uma pequena mudança na validação de e-mail. Além de validar se ele foi preenchido, vamos validar se o e-mail é válido.

Para isso, vamos criar um novo erro e um else para quando o valor do e-mail não esteja vazio.

...

ErrEmailRequired = errors.New("Email is required")

ErrEmailInvalid = errors.New("Email is invalid")

...

if p.Email == "" {
		return ErrEmailRequired
} else {
		rgx := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$")
		if !rgx.MatchString(p.Email) {
			return ErrEmailInvalid
		}
}
...

Agora, como boa parte do código do teste é igual ao anterior, vamos duplicar a função renomeando-a para FuzzValidateData.

func FuzzValidateData(f *testing.F) {
	testcases := []Person{
		{"Tiago Temporin", "tiago@aprendagolang.com.br", "1234", 32},
		{"Maria Castro", "maria.castro@algumdominio.com", "1Av6s#", 22},
		{"Daniela Fernandez", "dani@teste.com.br", "1234", 16},
	}

	for _, tc := range testcases { // seed
		data, _ := json.Marshal(tc)

		f.Add(data)
	}

	knowErrs := map[string]bool{
		ErrNameRequired.Error():   true,
		ErrEmailRequired.Error():  true,
		ErrPwdRequired.Error():    true,
		ErrPwdMinChars.Error():    true,
		ErrAgeMin.Error():         true,
		ErrInvalidPayload.Error(): true,
	}

	srv := httptest.NewServer(http.HandlerFunc(validate))
	defer srv.Close()

	f.Fuzz(func(t *testing.T, data []byte) {
		resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))
		if err != nil {
			t.Errorf("Error: %v", err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			if _, ok := knowErrs[string(body)]; ok {
				t.Skip(fmt.Sprintf("skiping knowing error: %s", body))
			}

			t.Errorf("Expected status code %d, got %d with error :%s", http.StatusOK, resp.StatusCode, body)
		}
	})
}

Modificando Fuzz Test

Como queremos que sejam gerados inputs para cada um dos campos que temos na nossa struct, precisamos modificar o looping onde fazemos o seed de dados para o fuzz.

for _, tc := range testcases {
    f.Add(tc.Name, tc.Email, tc.Password, tc.Age)
}

Feito isso, o próximo passo é alterar a função do f.Fuzz para “acomodar” os novos parâmetros.

f.Fuzz(func(t *testing.T, name, email, password string, age uint8) {

Dentro da função do f.Fuzz, como ela não recebe mais um slice bytes como parâmetro, antes de realizar a request, precisamos criar uma struct com os inputs recebidos e em seguida fazer o json.Marshal para gerar o slice de bytes que será enviado no POST.

p := Person{
	Name:     name,
	Email:    email,
	Password: password,
	Age:      age,
}

data, _ := json.Marshal(p)

O código completo ficará assim:

func FuzzValidateData(f *testing.F) {
	testcases := []Person{
		{"Tiago Temporin", "tiago@aprendagolang.com.br", "1234", 32},
		{"Maria Castro", "maria.castro@algumdominio.com", "1Av6s#", 22},
		{"Daniela Fernandez", "dani@teste.com.br", "1234", 16},
	}

	for _, tc := range testcases {
		f.Add(tc.Name, tc.Email, tc.Password, tc.Age)
	}

	knowErrs := map[string]bool{
		ErrNameRequired.Error():   true,
		ErrEmailRequired.Error():  true,
		ErrPwdRequired.Error():    true,
		ErrPwdMinChars.Error():    true,
		ErrAgeMin.Error():         true,
		ErrInvalidPayload.Error(): true,
	}

	srv := httptest.NewServer(http.HandlerFunc(validate))
	defer srv.Close()

	f.Fuzz(func(t *testing.T, name, email, password string, age uint8) {
		p := Person{
			Name:     name,
			Email:    email,
			Password: password,
			Age:      age,
		}

		data, _ := json.Marshal(p)

		resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))
		if err != nil {
			t.Errorf("Error: %v", err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			if _, ok := knowErrs[string(body)]; ok {
				t.Skip(fmt.Sprintf("skiping knowing error: %s", body))
			}

			t.Errorf("Expected status code %d, got %d with error :%s", http.StatusOK, resp.StatusCode, body)
		}
	})
}

Como temos 2 funções de fuzz e só podemos executar uma função por vez, precisamos adicionar um filtro na hora de executar o comando dos testes.

$ go test -fuzz ValidateData -fuzztime 5s .

Ao executar o comando acima, o seguinte erro foi exibido no terminal:

fuzz: elapsed: 0s, gathering baseline coverage: 0/65 completed
fuzz: minimizing 78-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 4/65 completed
--- FAIL: FuzzValidateData (0.10s)
    --- FAIL: FuzzValidateData (0.00s)
        main_test.go:99: Expected status code 200, got 400 with error :Email is invalid
    
    Failing input written to testdata/fuzz/FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4
    To re-run:
    go test -run=FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4
FAIL
exit status 1
FAIL    github.com/aprendagolang/httpfuzz       0.869s

Como podemos ver na mensagem de erro, o problema encontrado foi que o e-mail passado na request é inválido. Podemos confirmar isso ao abrir o arquivo que foi gerado em testdata/fuzz/FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4 onde podemos ver os valores passados como Name, Email, Password e Age.

go test fuzz v1
string("0")
string("0")
string("0")
byte('\\x10')

Para resolver esse problema, basta adicionar o erro que já havíamos definido à nossa lista de erros conhecidos na função FuzzValidateData.

knowErrs := map[string]bool{
	ErrNameRequired.Error():   true,
	ErrEmailRequired.Error():  true,
	ErrEmailInvalid.Error():   true,
	ErrPwdRequired.Error():    true,
	ErrPwdMinChars.Error():    true,
	ErrAgeMin.Error():         true,
	ErrInvalidPayload.Error(): true,
}

Ao executar o comando novamente, o resultado foi o seguinte:

fuzz: elapsed: 0s, gathering baseline coverage: 0/82 completed
fuzz: elapsed: 0s, gathering baseline coverage: 82/82 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 6078 (2026/sec), new interesting: 7 (total: 89)
fuzz: elapsed: 6s, execs: 6678 (200/sec), new interesting: 7 (total: 89)
fuzz: elapsed: 6s, execs: 6678 (0/sec), new interesting: 7 (total: 89)
PASS
ok      github.com/aprendagolang/httpfuzz       6.276s

Com isso fechamos os posts, pelo menos por enquanto, sobre como utilizar Fuzz Test em requests HTTP.

Espero que tenham gostado.

Até a próxima!


Se inscreva na nossa newsletter

* indicates required

Deixe uma resposta