Dua Bucket dan Satu Tutup: Meredam Endpoint Saran Kritik
Endpoint umpan balik publik butuh dua pertahanan sekaligus: batas ukuran badan permintaan sebelum decode dan dua bucket rate limit per akun setelah autentikasi.
Ringkasan
Endpoint form saran lolos tes fungsional, tapi review keamanan nemu dua bolong: body JSON tanpa batas ukuran dan nggak ada rate limit. Fixnya gampang: MaxBytesReader 32 KiB buat ukuran, plus dua bucket limit per akun (1 kirim per 10 menit, 5 per hari) yang dipasang setelah auth biar kuncinya ID user, bukan IP. Terakhir, array kosong admin jangan jadi null.
Formulir saran dan kritik di sebuah portal layanan publik baru saja tayang. Satu tombol kirim, dua kolom teks, tidak ada lampiran. Versi pertama endpoint-nya lolos uji fungsional, lalu sesi tinjauan keamanan menemukan dua hal yang tidak terlihat dari tes biasa: badan permintaan JSON diterima tanpa batas ukuran, dan tidak ada mekanisme apa pun yang mencegah satu akun mengirim seratus umpan balik berturut-turut. Dugaan awal, menambahkan validasi panjang kolom sudah cukup. Dugaan itu meleset karena panjang kolom dan laju permintaan adalah dua sumbu yang berbeda.
Dua Sumbu yang Butuh Dua Mekanisme
Sumbo pertama adalah ukuran. Setiap payload masuk perlu ditegakkan batas maksimalnya sebelum diolah, bukan setelahnya[3]. Di handler, badan permintaan dibungkus pembatas sebelum decoder JSON bekerja:
r.Body = http.MaxBytesReader(w, r.Body, 32<<10) // 32 KiB sebelum decode
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
response.Error(w, http.StatusBadRequest, "BAD_REQUEST", "body tidak valid")
return
}
Angka 32 KiB terdengar arbitrer sampai dihitung: dua kolom, masing-masing maksimal 5000 rune teks Indonesia, paling boros sekitar 10 KB. Batas 32 KiB memberi ruang lega bagi kiriman sah sekaligus menolak payload raksasa sebelum ia menyentuh memori. Validasi di lapisan layanan tetap berjalan: minimal salah satu kolom terisi, cap 5000 rune per kolom, dan sisi kosong disimpan sebagai NULL.
Sumbo kedua adalah frekuensi. Di sinilah rate limit masuk, dan panduan resminya tegas: rate limiting adalah kontrol fondasional yang diterapkan pada beberapa kunci sekaligus, bukan hanya IP[1]. Untuk identitas terautentikasi, penerapannya paling andal karena berlaku setelah login[1].
Urutan Middleware Menentukan Kunci Bucket
Kesalahan yang mudah terjadi: memasang limiter sebelum middleware autentikasi. Ketika itu terjadi, satu-satunya kunci yang tersedia adalah alamat IP, dan satu NAT kantor berisi puluhan pengguna asli berbagi satu bucket. Implementasi yang benar justru memasang limiter SETELAH autentikasi, sehingga kunci bucket adalah ID pengguna dari konteks request[1].
saranKritikLimiters := []func(http.Handler) http.Handler{
middleware.RateLimit(middleware.NewRateLimiter(1, 10*time.Minute), middleware.UserKeyFunc("saran-kritik-10m")),
middleware.RateLimit(middleware.NewRateLimiter(5, 24*time.Hour), middleware.UserKeyFunc("saran-kritik-24h")),
}
// limiter dipasang SETELAH requireUser supaya kunci memakai CtxUserID
saranKritikHandler.RegisterRoutes(api, middleware.RequiredUserAuth(cfg.JWTSecret), saranKritikLimiters...)
Dua bucket berantai menjawab dua pertanyaan bisnis sekaligus: satu kiriman per 10 menit mencegah banjir instan, maksimal lima per 24 jam menjaga total harian tetap masuk akal. Kedua bucket diperiksa independen, keduanya harus lolos. Limiter jendela-tetap di memori memang punya kelemahan yang diakui dokumentasi OWASP: lonjakan di tepi jendela[1]. Untuk satu instance dengan audiens terbatas, trade-off itu diterima secara sadar; keputusan soal angka limit memang harus disetel ke kebutuhan bisnis masing-masing endpoint[3].
Ketika Bucket Penuh: Kontrak 429
Saat limit tembus, server menjawab 429 Too Many Requests. RFC 6585 mendefinisikan kode ini untuk "terlalu banyak permintaan dalam rentang waktu tertentu", dan menyebut respons boleh menyertakan header Retry-After yang menandakan berapa lama menunggu sebelum mencoba lagi[2]. Respons 429 juga tidak boleh disimpan cache[2]. Pesan errornya sengaja generik, tanpa membocorkan bucket mana yang kena, sesuai anjuran agar respons tetap generik ketika limit tercapai[1].
Satu kontrak kecil hampir lolos dari perhatian: endpoint daftar untuk admin. Query MySQL yang mengembalikan nol baris menghasilkan nilai nil di Go, dan marshaling JSON default mengubahnya menjadi null. Kontrak OpenAPI menjanjikan array. Solusinya dua baris: bila rows nil, ganti dengan irisan kosong sebelum respons diserialisasi. Tanpa itu, klien yang mengandalkan tipe array akan crash di data kosong, bukan di data penuh.
Hasil akhirnya: satu endpoint publik dengan pertahanan pada dua sumbu. Ukuran ditutup MaxBytesReader, frekuensi ditutup dua bucket per akun, dan kontrak array kosong dijaga eksplisit. Tiga keputusan kecil yang masing-masing hanya beberapa baris, tapi hanya satu yang terlintas sebelum tinjauan keamanan berjalan.
Sumber
- OWASP Cheat Sheet Series — Bot Management and Anti-Automation: Rate Limiting and Quotas (diakses 2026-10-14).
- RFC 6585 — Additional HTTP Status Codes, Section 4: 429 Too Many Requests (diakses 2026-10-14).
- OWASP API Security Top 10 (2023) — API4:2023 Unrestricted Resource Consumption (diakses 2026-10-14).