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
stringhasil unmarshaling tanpa validasiutf8.ValidStringdapat 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
nilsetelah 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:
- End-to-End via
app.Test: Mengirimkan HTTP request sintetis ke Fiber router. Pendekatan ini memverifikasi integrasi middleware (sepertirecover.New()), namun eksekusi per detiknya lambat (sekitar 2.000–5.000 iterasi/detik) akibat overhead alokasi HTTP context dan routing table. - 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!