IP Klien yang Bisa Dipercaya di Balik Proxy
Header X-Forwarded-For bisa diisi sembarang oleh klien. Begini cara memilih alamat yang benar-benar bisa dipercaya di balik Cloudflare dan nginx.
Ringkasan
Gara-gara serangan tebak password, endpoint login dikasih rate limiter, tapi awalnya salah baca IP dari entri pertama X-Forwarded-For yang gampang dipalsuin penyerang. Sekarang urutan resolusinya diubah: CF-Connecting-IP dulu, lanjut entri terakhir XFF, X-Real-IP, lalu RemoteAddr. Rate limitnya juga per akun (IP plus email), bales 429 dengan Retry-After, plus ada pembersihan memori biar nggak bocor.
Sebuah portal administrasi mengalami lonjakan percobaan tebakan kata sandi. Tim pengembang segera menambahkan middleware pembatas laju pada endpoint login untuk meredam serangan tersebut. Namun, efektivitas pembatas laju tersebut sepenuhnya bergantung pada kejujuran alamat IP klien yang dibacanya.
Asumsi awal yang umum adalah mengambil entri pertama dari header X-Forwarded-For. Banyak tutorial menyarankan pola ini sebagai standar pengambilan alamat IP klien asal.
Di balik arsitektur Cloudflare dan nginx, entri pertama X-Forwarded-For sepenuhnya dapat dikendalikan oleh penyerang. Siapa pun dapat mengirimkan header palsu berisi X-Forwarded-For: 203.0.113.5. nginx menambahkan alamat koneksi yang dilihatnya sebagai entri terakhir melalui variabel $proxy_add_x_forwarded_for, yang menambahkan $remote_addr ke header tersebut [3]. Ketika lalu lintas datang dari Cloudflare, alamat yang dilihat nginx adalah IP edge-nya, sehingga entri terakhir tetap ditulis oleh pihak yang kita kendalikan. Ketika lalu lintas selalu melewati Cloudflare, header CF-Connecting-IP diatur oleh edge itu sendiri dan menjadi klaim yang paling langsung [2].
Header X-Forwarded-For bersifat hanya menambah (append-only). Entri paling kiri ditulis oleh pihak yang paling tidak terkontrol, sehingga tidak ada bagian dari header itu yang layak dipercaya untuk keputusan keamanan jika server masih bisa dijangkau secara langsung [1]. Kepercayaan adalah properti dari siapa yang menulis entri terakhir, bukan dari nama header itu sendiri.
Ada satu konsekuensi operasional dari aturan itu. Jika port aplikasi tersebut tidak sengaja bisa dijangkau langsung dari internet, meskipun secara normal ia berada di balik proxy, tidak ada satu pun entri X-Forwarded-For yang layak dipercaya untuk keperluan keamanan [1]. Rantai proxy harus benar-benar menjadi satu-satunya pintu, misalnya lewat aturan firewall atau binding ke antarmuka internal, sebelum logika pembacaan header seperti ini bisa diandalkan.
Urutan Resolusi dan Batas Penyaringan
Implementasi resolusi alamat pada clientip.go mengikuti urutan ketat untuk mengatasi kompleksitas ini. Header CF-Connecting-IP diperiksa pertama. Jika tidak ada, sistem fallback ke entri terakhir X-Forwarded-For, lalu X-Real-IP, dan akhirnya TCP RemoteAddr dengan port yang dihapus menggunakan net.SplitHostPort. Urutan ini memastikan alamat yang paling tepercaya dipilih terlebih dahulu.
Pada middleware/ratelimit.go, digunakan pembatas laju jendela tetap (fixed-window) dalam memori. Fungsi Allow(key) mengembalikan dua nilai: keputusan boolean dan sisa waktu jendela bila permintaan ditolak. Penolakan menghasilkan respons HTTP 429 [4], kode kesalahan JSON RATE_LIMITED, pesan terlalu banyak percobaan; coba lagi nanti, serta header Retry-After dalam detik yang dihitung dari sisa jendela ditambah satu [5]. Spesifikasi RFC secara eksplisit menyatakan bahwa server harus menjelaskan kondisi pembatasan, namun tidak mendefinisikan bagaimana server mengidentifikasi pengguna atau menghitung permintaan.
Kunci Per Akun dan Kontrak 429
Fungsi LoginKeyFunc membentuk kunci unik dengan format prefix|IP|email. Email dibaca dari badan permintaan JSON. Badan tersebut dikuras melalui io.LimitReader dengan batas 64KB dan dipulihkan melalui io.NopCloser agar handler hilir tetap dapat membacanya [7]. Pendekatan ini memastikan satu akun yang terkunci tidak menghukum akun lain di balik alamat yang sama. Membatasi laju berdasarkan alamat saja rentan terhadap pengguna jaringan bersama (NAT), sehingga mengikat batas laju pada identitas pengguna adalah praktik yang lebih aman sesuai rekomendasi OWASP [6].
Fungsi sweepLocked menghapus jendela kedaluwarsa sekali peta memori melewati 10000 kunci. Tanpa mekanisme pembersihan ini, serangan flooding dengan kunci acak akan menyebabkan kebocoran memori hingga server kehabisan sumber daya.
Angka 10000 bukan batas magic, melainkan pemicu pembersihan berkala: peta hanya disweep ketika ukurannya melampaui ambang itu, dan hanya jendela yang sudah kedaluwarsa yang dibuang. Serangan yang membombardir endpoint dengan kunci palsu tetap membuat peta tumbuh sesaat, tetapi memorinya terikat di sekitar ambang tersebut alih-alih tumbuh tanpa batas.
Konsekuensi dari penguraian entri pertama yang lama sangat fatal. Penyerang dapat memutar entri pertama XFF palsu secara acak pada setiap permintaan. Setiap permintaan akan terlihat berasal dari klien baru, sehingga pembatas laju tidak pernah aktif. Lubang keamanan yang sama sebelumnya juga ditemukan pada kolom ip_address di sistem tampilan dedup dan ulasan, yang memungkinkan manipulasi data statistik.
Perbaikan pada kolom pencatatan IP juga ikut serta. Penghitung dedup tampilan dan kolom ip_address pada ulasan memakai fungsi resolusi yang sama, sehingga statistik yang selama ini bisa dimanipulasi dengan header palsu kini mencatat alamat dari sumber yang sama tepercayanya dengan kunci rate limiter.
Arsitektur proxy modern menuntut evaluasi ulang terhadap asumsi jaringan dasar. Ketika permintaan melewati lebih dari dua lapisan proxy atau penyeimbang beban yang berbeda, bagaimana strategi validasi alamat ini harus beradaptasi tanpa mengorbankan performa?
Sumber
[1] MDN, X-Forwarded-For header, developer.mozilla.org
[2] Cloudflare Fundamentals, HTTP request headers, developers.cloudflare.com
[3] nginx, Module ngx_http_proxy_module ($proxy_add_x_forwarded_for), nginx.org
[4] RFC 6585, Additional HTTP Status Codes, rfc-editor.org
[5] RFC 9110, HTTP Semantics, Retry-After, httpwg.org
[6] OWASP Authentication Cheat Sheet, Login Throttling, cheatsheetseries.owasp.org
[7] Go io package, LimitReader, pkg.go.dev