La nueva versión de Go (1.18) esta cerca de ser lanzada, una de las novedades, que ya podemos probar es Fuzzing, estamos hablando de Fuzz Testing de manera nativa en Go. Para ponernos en contexto.
¿Qué es Fuzz Testing?
Es una técnica de testing automatizado con el propósito de encontrar vulnerabilidades dando entradas invalidas a un sistema o software.
Es practicado más comúnmente por hackers e ingenieros de seguridad, los primeros para aprovechar las vulnerabilidades y los segundos para arreglarlas antes de que estas puedan ser explotadas.
El fuzz testing involucra cantidades masivas de información que es generada de manera aleatoria con la cual será puesto a prueba el sistema con el objetivo de lograrlo romper.
Este método es especialmente útil para encontrar bugs y casos extremos que como humanos es muy fácil olvidar o no tener en cuenta al momento de que probamos nuestros servicios.
Go Fuzzing
Ahora que sabemos de que hablamos, ¿Cómo puedo usarlo en mis proyectos en Go? Como ya mencionamos esta característica esta pensada para la version 1.18 de go, que actualmente está en beta, pero afortunadamente podemos probar ya.
Instalar version 1.18 Beta
Corremos el siguiente comando para instalar la versión beta.
$ go install golang.org/dl/go1.18beta1@latest
Descargamos actualizaciones.
$ go1.18beta1 download
Para este tutorial debemos usar la versión beta.
$ go1.18beta1 version
O usar un alias para mayor comodidad
$ alias go=go1.18beta1
$ go version
Creando el código a testear
- Vamos a crear una nueva carpeta
$ mkdir fuzz
$ cd fuzz
- Corremos el siguiente comando para definir un modulo
$ go mod init ejemplo/fuzz
- Vamos a crear un nuevo archivo
main.go
con el siguiente método, su función es simple, recibir una cadena e invertirla
|
|
Al correr nuestro programa beberíamos obtener una salida similar
$ go run .
original: "Anita lava la tina"
Invertida: "anit al aval atinA"
Invertir una vez más: "Anita lava la tina"
Añadiendo test unitarios
- Crearemos un archivo
main_test.go
con el siguiente código.
|
|
En este test solo vamos a probar que nuestra función invierta correctamente la cadena, por lo que si lo corremos debería pasar sin mucho problema
$ go test
PASS
ok ejemplo/fuzz 0.013s
Añadiendo fuzz test
Puntos importantes a tener en cuenta con respecto a los fuzz test
- Deben ser nombrados con la siguiente nomenclatura FuzzNombre, empezar por Fuzz seguido del nombre que quieras darle
- Los argumentos deben ser:
- string, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8/byte, uint16, uint32, uint64
- float32, float64
- bool
- Deben estar en los archivos con terminación _test.go
Creamos nuestro fuzz test dentro de main_test.go
, no olvidemos importar los nuevos paquetes que vamos a utilizar.
|
|
A diferenciá de los unit test en los que normalmente damos una entrada y sabemos exactamente que salida esperamos, en los fuzz test, dado que recibimos datos generados de manera aleatoria, no podemos predecir la salida esperada. Es por eso que en nuestras pruebas estamos verificando que al aplicar nuevamente nuestro método, recibamos la cadena original.
Tenemos dos opciones para correr nuestros tests:
- La forma por default:
go test .
Si hacemos esto, nuestro fuzz test hará la prueba con los valores con que alimentamos nuestro corpus, No va a generar algún valor - Usando la bandera
-fuzz
:go test . -fuzz=NombreDelTest
Si usamos esta alternativa nuestro fuzz test empezara a generar datos de manera aleatoria para probar nuestro código.
Probemos el código
- Primero corramos nuestro código de manera nativa y verifiquemos que nuestros valores de corpus pasan correctamente
go test
PASS
ok ejemplo/fuzz 0.672s
- Ahora corramos nuestro código con la bandera fuzz
go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 12 workers
fuzz: minimizing 33-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzInvertir (0.06s)
--- FAIL: FuzzInvertir (0.00s)
main_test.go:37: Invertir no devolvio una cadena UTF-8 valida "\x8e\xcf"
Failing input written to testdata/fuzz/FuzzInvertir/ac96f6f1a42cb9a37e2d3e4c0a98c6d43339e291d7c8f715f7254b20f00e146c
To re-run:
go test -run=FuzzInvertir/ac96f6f1a42cb9a37e2d3e4c0a98c6d43339e291d7c8f715f7254b20f00e146c
FAIL
exit status 1
FAIL ejemplo/fuzz 0.609s
Como vemos nuestro código ah fallado, a partir de este momento tenemos una nueva entrada para nuestro corpus, la entrada con la que nuestro test fallo, el cual podemos ver en el archivo generado.
go test fuzz v1
string("ώ")
Si volvemos a correr el comando go test .
nuestras pruebas fallaran porque ahora el test incluye la entrada invalida que encontró previamente.
Arreglando el error
Si gustas, eres libre de buscar el problema por ti. En este tutorial, vamos a usar la terminar para buscar el error, por el mensaje recibido sabemos que nuestra salida UTF-8 valido.
Vamos a añadir la siguiente línea de código para obtener más información.
|
|
Corremos nuevamente con la bandera -v go test -v .
go test -v .
=== RUN TestInvertir
--- PASS: TestInvertir (0.00s)
=== RUN FuzzInvertir
=== RUN FuzzInvertir/seed#0
main_test.go:33: Numero de runas: orig=10, rev=10, dobleRev=10
=== RUN FuzzInvertir/seed#1
main_test.go:33: Numero de runas: orig=1, rev=1, dobleRev=1
=== RUN FuzzInvertir/seed#2
main_test.go:33: Numero de runas: orig=6, rev=6, dobleRev=6
=== RUN FuzzInvertir/ac96f6f1a42cb9a37e2d3e4c0a98c6d43339e291d7c8f715f7254b20f00e146c
main_test.go:33: Numero de runas: orig=1, rev=2, dobleRev=1
main_test.go:38: Invertir no devolvio una cadena UTF-8 valida "\x8e\xcf"
--- FAIL: FuzzInvertir (0.00s)
--- PASS: FuzzInvertir/seed#0 (0.00s)
--- PASS: FuzzInvertir/seed#1 (0.00s)
--- PASS: FuzzInvertir/seed#2 (0.00s)
--- FAIL: FuzzInvertir/ac96f6f1a42cb9a37e2d3e4c0a98c6d43339e291d7c8f715f7254b20f00e146c (0.00s)
FAIL
FAIL ejemplo/fuzz 0.411s
FAIL
Podemos ver que los valores con los que poblamos nuestro corpus, todos son cadenas, en los cuales los caracteres necesitan un solo byte, pero con el carácter ώ requiere más, por lo que al intentar invertirlo byte por byte resulta en un carácter invalido. Así que vamos a solucionar el error
Corrigiendo el error
La solución es simple, vamos invertirlo runa por runa en lugar de usar los bytes
func Invertir(s string) string {
b := []rune(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
Si volvemos a ejecutar nuestros test:
go test .
ok ejemplo/fuzz 0.549s
Nuestros tests pasaron exitosamente. Muy bien! ahora tenemos que correr nuevamente nuestros fuzz test en busca de algún error o caso que no contemplamos.
go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed
fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 12 workers
fuzz: minimizing 49-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzInvertir (0.04s)
--- FAIL: FuzzInvertir (0.00s)
main_test.go:33: Numero de runas: orig=1, rev=1, dobleRev=1
main_test.go:35: Antes de invertir: "\xdf", despues del invertir: "�"
Failing input written to testdata/fuzz/FuzzInvertir/6d1afba479d1e743926c35fff31a09168a87c4c416f6d927c76d506f3c63ba08
To re-run:
go test -run=FuzzInvertir/6d1afba479d1e743926c35fff31a09168a87c4c416f6d927c76d506f3c63ba08
FAIL
exit status 1
FAIL ejemplo/fuzz 0.512s
Vemos que ahora el error es causado porque la cadena no es la misma al invertirla por segunda vez, esto es debido a que la entrada no es un carácter UTF-8 valido.
Arreglando doble inversion
Como habíamos mencionado, la entrada es un slice de bytes con un solo byte \xdf
, por lo que al convertirlo a un []rune
, Go hace un encode a UTF-8 remplazando el byte por el siguiente carácter �. Vamos a agregar las siguientes líneas para obtener más información.
func Invertir(s string) string {
fmt.Printf("entrada: %q\n", s)
b := []rune(s)
fmt.Printf("runas: %q\n", b)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
Vamos a correr nuestro fuzz test usando la bandera -run
para correr únicamente el test que nos interesa inspeccionar.
go test -run=FuzzInvertir/6d1afba479d1e743926c35fff31a09168a87c4c416f6d927c76d506f3c63ba08
entrada: "\xdf"
runas: ['�']
entrada: "�"
runas: ['�']
--- FAIL: FuzzInvertir (0.00s)
--- FAIL: FuzzInvertir/6d1afba479d1e743926c35fff31a09168a87c4c416f6d927c76d506f3c63ba08 (0.00s)
main_test.go:33: Numero de runas: orig=1, rev=1, dobleRev=1
main_test.go:35: Antes de invertir: "\xdf", despues del invertir: "�"
FAIL
exit status 1
FAIL ejemplo/fuzz 0.257s
Como vemos, podemos confirmar que la entrada no es un carácter unicode valido. Vamos a solucionar ese escenario. Si detectamos que la entrada es un carácter invalido regresaremos un error, tendremos que modificar la firma de nuestro método y hacer ajustes en nuestro código para soportar la actualización de nuestro método
|
|
De igual manera tendremos que modificar nuestros tests y si encontramos un error saltar ese escenario.
|
|
Podemos correr nuevamente nuestros tests.
go test .
ok ejemplo/fuzz 0.546s
Bien, vemos que ya quedo corregido el escenario que no soportamos. Ahora volvamos a correr nuestros fuzz test. Los Fuzz test seguirá ejecutándose hasta encontrar algún error, de no ser así podemos detenerlos con ctrl-c
go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 12 workers
fuzz: elapsed: 3s, execs: 408367 (136118/sec), new interesting: 35 (total: 35)
fuzz: elapsed: 6s, execs: 840045 (143895/sec), new interesting: 36 (total: 36)
...
fuzz: elapsed: 48s, execs: 890470 (0/sec), new interesting: 36 (total: 36)
^Cfuzz: elapsed: 51s, execs: 890470 (0/sec), new interesting: 36 (total: 36)
PASS
ok ejemplo/fuzz 51.083s
Podemos correrlos con la bandera -fuzztime
para limitar el tiempo de ejecución
go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/41 completed
fuzz: elapsed: 0s, gathering baseline coverage: 41/41 completed, now fuzzing with 12 workers
fuzz: elapsed: 3s, execs: 356810 (118815/sec), new interesting: 5 (total: 41)
fuzz: elapsed: 6s, execs: 463801 (35697/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 9s, execs: 470249 (2148/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 12s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 15s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 18s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 21s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 24s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 27s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 30s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
fuzz: elapsed: 31s, execs: 470249 (0/sec), new interesting: 6 (total: 42)
PASS
ok ejemplo/fuzz 31.508s
Conclusion
Genial! Ahora sabemos sobre fuzz testing y como podremos trabajar con el en Go. Este fue una introducción simple a sus características, pero sin duda es una herramienta muy útil para encontrar fallas en nuestro código. Si tienes una duda o comentario no dudes en contactarme por alguna de mis redes
Este post está basado y fue traducido de la documentación original de go Tutorial: Getting started with fuzzing.