Anatomi Masalah: Parser Crash dan Unhandled Panic pada Go Fiber

Go Fiber mengandalkan mekanisme deserialisasi cepat (seperti paket internal fasthttp dan sonic atau encoding/json) untuk memproses payload HTTP masuk. Pada implementasi endpoint produksi, kegagalan umum jarang berasal dari payload valid, melainkan dari input ekstrem yang tidak diprediksi saat unit test konvensional ditulis.

Beberapa skenario input penyebab crash runtime atau fatal panic meliputi:

  • Malformed UTF-8 Sequence: Akses indeks byte mentah pada string hasil unmarshaling tanpa validasi utf8.ValidString dapat merusak logika substring dan memicu slice bounds out of range.
  • Integer Overflow dan Underflow: Konversi numerik manual dari field interface atau string ke integer ukuran kecil (int8, int16) tanpa bounds-checking yang ketat.
  • Deeply Nested JSON Object: Struktur JSON bersarang hingga ratusan lapis yang memicu konsumsi stack berlebihan atau kehabisan alokasi memori (OOM exhaustion).
  • Nil-Pointer Dereference pada Custom Binder: Akses langsung ke pointer nested struct tanpa pemeriksaan nil setelah deserialisasi parsial.

Fuzz testing dengan testing.F (native sejak Go 1.18) mengeksplorasi ribuan mutasi input per detik secara terarah menggunakan pemandu coverage (coverage-guided fuzzing) untuk menemukan payload pembobol ini secara otomatis.

Strategi Fuzzing: app.Test vs Isolasi Parser Logic

Terdapat dua pendekatan untuk menyusun fuzz target pada aplikasi Fiber:

  1. End-to-End via app.Test: Mengirimkan HTTP request sintetis ke Fiber router. Pendekatan ini memverifikasi integrasi middleware (seperti recover.New()), namun eksekusi per detiknya lambat (sekitar 2.000–5.000 iterasi/detik) akibat overhead alokasi HTTP context dan routing table.
  2. Isolasi Fungsi Parser: Memisahkan logika binding, sanitasi, dan validasi dari transport layer HTTP ke dalam fungsi murni (pure function) bertipe func([]byte) (*Payload, error). Pendekatan ini mampu mengeksekusi 100.000+ iterasi per detik, mempercepat pendeteksian crash.

Praktek terbaik arsitektur Go adalah menguji parser murni dengan fuzzing intensif, lalu memverifikasi integrasi HTTP route secara terpisah.

Implementasi Fuzz Target dengan testing.F

Berikut adalah contoh implementasi payload parser kustom yang dipisahkan dari layer HTTP dan diuji menggunakan testing.F.

package parser_test

import (
	"bytes"
	"encoding/json"
	"errors"
	"testing"
	"unicode/utf8"
)

type UserRegistrationPayload struct {
	Username string `json:"username"`
	Age      int    `json:"age"`
	Metadata struct {
		TierCode string `json:"tier_code"`
	} `json:"metadata"`
}

func ParseAndValidateRegistration(raw []byte) (*UserRegistrationPayload, error) {
	if !utf8.Valid(raw) {
		return nil, errors.New("invalid UTF-8 payload")
	}

	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.DisallowUnknownFields()

	var payload UserRegistrationPayload
	if err := dec.Decode(&payload); err != nil {
		return nil, err
	}

	// Boundary assertions pencegah panic
	if len(payload.Username) > 50 {
		return nil, errors.New("username exceeds limit")
	}
	if payload.Age < 0 || payload.Age > 150 {
		return nil, errors.New("age out of valid range")
	}

	return &payload, nil
}

Selanjutnya, susun fuzz target dengan seed corpus yang relevan:

func FuzzParseAndValidateRegistration(f *testing.F) {
	// Seed Corpus: Input normal, batas, dan malformed awal
	f.Add([]byte(`{"username":"user1","age":25,"metadata":{"tier_code":"GOLD"}}`))
	f.Add([]byte(`{"username":"","age":0,"metadata":{"tier_code":""}}`))
	f.Add([]byte(`{}`))
	f.Add([]byte(`{"age": -5}`))
	f.Add([]byte("\xff\xfe\xfd")) // Broken sequence byte

	f.Fuzz(func(t *testing.T, data []byte) {
		payload, err := ParseAndValidateRegistration(data)
		if err != nil {
			// Expected rejection: error ditangani secara graceful
			return
		}

		// Invariant assertion: jika payload lolos, struktur harus memenuhi kontrak
		if payload == nil {
			t.Fatal("invariant violated: error is nil but payload is nil")
		}
		if payload.Age < 0 || payload.Age > 150 {
			t.Fatalf("invariant violated: invalid age escaped parser: %d", payload.Age)
		}
	})
}

Fuzzing Melalui HTTP Engine Fiber (app.Test)

Jika validasi custom binder bergantung pada context Fiber (misal: c.BodyParser()), gunakan app.Test untuk menguji ketahanan handler terhadap unhandled panic:

package parser_test

import (
	"bytes"
	"net/http/httptest"
	"testing"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/recover"
)

func FuzzFiberHandler(f *testing.F) {
	app := fiber.New()
	// Middleware recover wajib aktif untuk menangkal server fatal crash
	app.Use(recover.New())

	app.Post("/register", func(c *fiber.Ctx) error {
		var req UserRegistrationPayload
		if err := c.BodyParser(&req); err != nil {
			return c.Status(fiber.StatusBadRequest).SendString(err.Error())
		}
		return c.SendStatus(fiber.StatusOK)
	})

	f.Add([]byte(`{"username":"alpha","age":30}`))
	f.Add([]byte(`{"username":`))

	f.Fuzz(func(t *testing.T, body []byte) {
		req := httptest.NewRequest("POST", "/register", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")

		resp, err := app.Test(req, -1)
		if err != nil {
			t.Fatalf("app.Test failed execution: %v", err)
		}
		defer resp.Body.Close()

		// Handler tidak boleh memuntahkan 500 jika panic tertangkap oleh middleware
		if resp.StatusCode == fiber.StatusInternalServerError {
			t.Errorf("unhandled server fault (500) triggered by input: %q", body)
		}
	})
}

Menjalankan Fuzzing dan Analisis Crash

Jalankan fuzz engine melalui CLI Go:

go test -fuzz=FuzzParseAndValidateRegistration -fuzztime=30s

Jika ditemukan input yang memicu fatal error atau pelanggaran assertion, Go akan menghentikan pengujian dan menyimpan input tersebut di dalam direktori testdata/fuzz/FuzzParseAndValidateRegistration/<hash>.

File crash dump tersebut dapat langsung dieksekusi ulang secara deterministik menggunakan runner unit test standar:

go test -run=FuzzParseAndValidateRegistration/<hash>

Integrasi Pipeline CI/CD

Fuzzing tanpa batas waktu tidak realistis untuk continuous integration. Batasi eksekusi menggunakan flag -fuzztime agar pipeline berfungsi sebagai regression prevention workflow tanpa memperlambat deploy.

# .github/workflows/test.yml
name: Test Suite

on: [push, pull_request]

jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Run Unit Tests
        run: go test -v ./...

      - name: Run Fuzz Regression (Corpus Check)
        run: go test -run=^Fuzz ./...

      - name: Run Time-Limited Fuzzing
        run: go test -fuzz=FuzzParseAndValidateRegistration -fuzztime=60s -run=^$ ./...
Catatan: Perintah -run=^$ memastikan unit test reguler tidak dijalankan ulang selama sesi fuzzing berlangsung, sehingga alokasi 60 detik sepenuhnya dialokasikan untuk mutasi input fuzz engine.