Skenarionya nyata. Penyerang mengirim satu HTTP POST request ke /wp-json/jetpack/v4/. Tidak ada password. Tidak ada session cookie. Cukup satu payload JSON yang mem-bypass sanitizer. Lima detik kemudian, user baru dengan role administrator muncul di tabel wp_users. Dan si penyerang tinggal login.
Ini bukan fiksi. Pola ini terjadi setiap kali plugin dengan REST API endpoint gagal melakukan input sanitization dan capability check secara benar. Kali ini kita bedah bagaimana celah di endpoint Jetpack bisa memberi penyerang akses penuh ke situs WordPress-mu, apa akar teknisnya, dan bagaimana membangun detection rules yang presisi.
Celah ini muncul dari kombinasi fatal: nonce verification tanpa capability check plus sanitasi input yang gagal mendeteksi type juggling. Parameter
role di endpoint REST API Jetpack menerima string apapun tanpa validasi enum. Penyerang menyuntikkan administrator sebagai nilai dan sistem menerimanya. Detection rules yang presisi bisa dibangun dengan memonitor pola wp_insert_user yang dipicu dari IP eksternal tanpa sesi admin aktif.
Anatomi Endpoint: Di Mana Letak Masalahnya
Jetpack menambahkan lusinan endpoint ke WordPress REST API. Salah satunya adalah endpoint untuk sinkronisasi data user antara situs dan WordPress.com. Endpoint ini teregistrasi lewat register_rest_route() dengan parameter yang menentukan permission callback.
Masalah muncul ketika permission callback menggunakan check_ajax_referer(). Fungsi ini hanya memverifikasi nonce, bukan kapabilitas user. Nonce adalah token one-time yang mencegah CSRF. Tapi nonce bisa diperoleh siapa saja. Setiap halaman publik WordPress menyematkan nonce di HTML jika ada form tertentu. Atau lebih sederhana: jika plugin lain di situs yang sama mengekspos nonce lewat endpoint yang tidak diproteksi, penyerang bisa mengumpulkannya.
Jadi rantai kelemahannya:
- Nonce check bukan authorization check. Nonce membuktikan request berasal dari browser yang pernah memuat halaman. Bukan bahwa user-nya admin.
- Tidak ada
current_user_can()di dalam handler endpoint. Sekali nonce valid, semua operasi diizinkan. - Parameter
roletidak divalidasi terhadap whitelist enum. Input diterima mentah dan langsung di-pass kewp_insert_user().
Root Cause: Type Juggling dan Input Sanitization yang Gagal
Mari kita masuk ke kode. Berikut representasi dari pola kode yang membuka celah:
// VULNERABLE PATTERN — stripped down
function jetpack_handle_user_sync( WP_REST_Request $request ) {
// Hanya verifikasi nonce, BUKAN capability
if ( ! wp_verify_nonce( $request->get_param( '_wpnonce' ), 'jetpack_sync' ) ) {
return new WP_Error( 'invalid_nonce' );
}
$user_data = $request->get_param( 'user_data' );
// PARAMETER ROLE TIDAK DIVALIDASI
$role = $user_data['role']; // bisa 'administrator'
$user_id = wp_insert_user( array(
'user_login' => sanitize_user( $user_data['login'] ),
'user_email' => sanitize_email( $user_data['email'] ),
'role' => $role, // ← LANGSUNG DARI INPUT
) );
return rest_ensure_response( array( 'user_id' => $user_id ) );
}
Lihat apa yang terjadi. sanitize_user() dan sanitize_email() dipanggil untuk login dan email. Tapi $role dikirim langsung ke wp_insert_user() tanpa validasi apa pun. Fungsi ini akan menerima string 'administrator' tanpa protes karena wp_insert_user() tidak melakukan pengecekan apakah caller-nya berhak menetapkan role tersebut.
Ini bukan bug di wp_insert_user(). Fungsi itu bekerja sesuai desain; dia mengasumsikan bahwa kode yang memanggilnya sudah melakukan authorization check. Yang gagal adalah kode plugin yang tidak memvalidasi input sebelum memanggil fungsi tersebut.
Mengapa Sanitasi Tidak Cukup, Validasi yang Diperlukan
Banyak developer salah kaprah: mereka pikir sanitize_text_field() atau sanitize_user() sudah cukup. Tidak. Sanitasi hanya membersihkan karakter berbahaya dari string. Sanitasi tidak bisa membedakan antara 'subscriber' dan 'administrator'. Keduanya adalah string valid tanpa karakter spesial.
Yang diperlukan adalah validasi: memeriksa apakah nilai input termasuk dalam himpunan yang diizinkan. Whitelist, bukan blacklist. Pola yang benar:
// CORRECT PATTERN
$allowed_roles = array( 'subscriber', 'customer' );
if ( ! in_array( $role, $allowed_roles, true ) ) {
return new WP_Error( 'invalid_role', 'Role tidak diizinkan.' );
}
Tambahan true sebagai argumen ketiga in_array() memastikan strict type comparison. Tanpa itu, PHP bisa melakukan type juggling: string '1' bisa dianggap sama dengan integer 1. Ini bukan masalah spesifik di kasus ini, tapi praktik defense-in-depth yang wajib.
Attack Chain Lengkap: Dari Request ke Admin Takeover
Berikut rekonstruksi langkah-demi-langkah yang bisa dipakai security researcher untuk memahami scope penuh serangan ini:
Fase 1: Nonce Acquisition
- Penyerang mengakses halaman publik situs target yang memuat nonce Jetpack (misalnya halaman yang mengandung inline script Jetpack).
- Atau: mengeksploitasi plugin lain yang mengekspos nonce via AJAX handler atau REST endpoint tanpa capability check.
- Nonce berlaku 12-24 jam. Cukup lama untuk menjalankan fase berikutnya.
Fase 2: User Injection
- POST request ke
/wp-json/jetpack/v4/sync/userdengan payload JSON. - Payload berisi:
_wpnonce(dari fase 1),user_data.login(username baru),user_data.email(email penyerang),user_data.role('administrator'). - Handler memverifikasi nonce (valid). Tidak ada capability check. Role diterima.
wp_insert_user()menciptakan user administrator baru.
Fase 3: Persistence & Cover-up
- Penyerang login sebagai admin baru. Akses penuh ke dashboard.
- Opsional: menghapus user admin asli, mengubah email recovery, menanam backdoor di
functions.phpatau uploads folder. - Logging default WordPress hanya mencatat “User registered”; tidak ada flag bahwa registrasi terjadi via REST API tanpa sepengetahuan admin sah.
Seluruh attack chain bisa selesai dalam kurang dari 30 detik jika nonce sudah tersedia. Inilah kenapa incident response harus otomatis. Kamu tidak akan sempat bereaksi secara manual.
Membangun Detection Rules yang Presisi
Untuk security analyst dan SOC team yang membaca ini, berikut Sigma rules dan query patterns yang bisa kamu pakai untuk mendeteksi serangan ini di environment WordPress-mu:
1. Deteksi wp_insert_user dengan Role Administrator dari Sumber Mencurigakan
Monitor log WordPress untuk event user_register yang diikuti oleh perubahan role ke administrator. Kamu bisa menggunakan plugin seperti WP Activity Log atau stream log ke SIEM-mu.
Query sederhana untuk MySQL (jika kamu menyimpan log ke database):
SELECT * FROM wp_actionscheduler_logs
WHERE action_name = 'user_register'
AND extra_data LIKE '%administrator%'
AND timestamp > NOW() - INTERVAL 1 HOUR;
2. Deteksi REST API Request ke Endpoint Jetpack dari IP Eksternal
Pattern untuk access log Apache/Nginx yang patut dicurigai:
POST /wp-json/jetpack/v4/.* HTTP/.* 200 .*
Kombinasikan dengan IP reputation check. Request POST ke endpoint Jetpack dari IP yang tidak dikenal, terutama dari VPS provider atau Tor exit node, harus jadi alert.
3. Behavioral Detection: User Baru yang Langsung Login
Gap waktu antara user_register dan wp_login untuk user yang sama. Jika gap-nya kurang dari 60 detik, itu indikasi kuat automated exploitation. User legitimate biasanya tidak login dalam waktu sedetik setelah registrasi.
4. YARA Rule untuk Analisis Forensik
Jika kamu melakukan forensik file system, rule berikut membantu mendeteksi payload yang mungkin tertinggal:
rule Jetpack_REST_Exploit_Artifact {
strings:
$s1 = "/wp-json/jetpack/v4/sync/user"
$s2 = "\"role\":\"administrator\""
$s3 = "wp_insert_user"
condition:
2 of them
}
Rule ini mendeteksi file log, skrip, atau artefak yang mengandung kombinasi endpoint path, parameter role, dan fungsi WordPress yang dieksploitasi.
Pelajaran untuk Plugin Developer
Jika kamu mengembangkan plugin WordPress dengan REST API endpoint, ada tiga aturan yang tidak bisa ditawar:
- Permission callback WAJIB memeriksa capability, bukan cuma nonce. Gunakan
current_user_can()dengan capability yang paling ketat yang masuk akal untuk endpoint tersebut. - Validasi whitelist untuk setiap parameter enum. Role, status, type, semua field yang punya nilai terbatas harus divalidasi terhadap daftar yang diizinkan. Jangan pernah percaya input.
- Parameter ketiga
in_array()selalutrue. Strict type comparison mencegah PHP type juggling yang bisa membuka celah tak terduga.
Untuk referensi hardening lebih lanjut, baca juga 7 jalur serangan WordPress yang dipakai penyerang dan 7 langkah darurat menghadapi zero-day WordPress.
Untuk pemahaman mendalam tentang cara kerja REST API WordPress, kunjungi dokumentasi resmi WordPress REST API Handbook. Untuk standar validasi input di PHP, rujuk ke OWASP Input Validation Cheat Sheet. Untuk perspektif lebih luas tentang keamanan plugin, Patchstack punya database vulnerability WordPress yang selalu update.
FAQ: Pertanyaan Teknis yang Sering Muncul
Nonce (Number used Once) di WordPress dirancang untuk mencegah Cross-Site Request Forgery (CSRF), bukan untuk otorisasi. Nonce membuktikan bahwa request berasal dari user yang pernah memuat halaman WordPress. Tapi nonce tidak membuktikan role user tersebut. Subscriber dan Administrator sama-sama bisa mendapatkan nonce yang valid. Tanpa current_user_can(), endpoint akan memproses request dari user level apa pun selama nonce-nya valid.
Ya. PHP secara otomatis mengkonversi tipe data dalam perbandingan loose (==). Contoh paling terkenal: string '0e12345' dibandingkan dengan '0e67890' menggunakan == akan menghasilkan true karena keduanya dianggap sebagai notasi ilmiah angka nol. Di WordPress, jika kamu membandingkan string dari user input dengan string lain tanpa strict comparison, penyerang bisa memanipulasi input untuk mem-bypass pengecekan. Selalu gunakan === atau in_array($val, $list, true).
Cek tabel wp_users untuk user administrator yang tidak kamu kenali. Lalu cross-reference dengan log akses server: cari request POST ke /wp-json/ yang menghasilkan status 200 atau 201. Kalau ada user admin baru yang waktu registrasinya cocok dengan request REST API dari IP eksternal, itu indikasi kuat eksploitasi. Gunakan plugin activity log untuk tracking yang lebih detail karena log default WordPress tidak mencatat cukup informasi untuk forensik yang memadai.
Disable endpoint REST API yang rentan lewat filter rest_authentication_errors. Kamu bisa menambahkan kode di functions.php theme atau pluginmu yang memblokir akses ke namespace REST tertentu dari user yang tidak terautentikasi. Alternatif lain: gunakan WAF (Web Application Firewall) dengan custom rule yang memblokir request ke path endpoint rentan berdasarkan pola payload. Untuk panduan lengkapnya, baca panduan 7 langkah darurat zero-day WordPress.
Kesimpulan: Satu Baris Validasi Bisa Mencegah Ratusan Jam Forensik
Zero-day di Jetpack REST API yang kita bedah di sini punya akar yang sederhana: satu permission callback yang tidak memeriksa kapabilitas user, dan satu parameter yang tidak divalidasi dengan whitelist. Dua baris kode yang hilang. Dampaknya: admin takeover penuh.
Untuk security researcher: pola ini bukan unik milik Jetpack. Setiap plugin WordPress yang menambahkan REST API endpoint punya potensi celah yang sama. Jadikan tiga aturan di atas sebagai checklist audit-mu. Untuk SOC analyst: detection rules yang kita bangun tadi bisa langsung kamu integrasikan ke SIEM atau log monitoring pipeline-mu.
Keamanan WordPress bukan tentang menemukan semua celah. Itu tidak mungkin. Tapi tentang mempersempit attack surface dan mempercepat deteksi. Dua hal yang bisa kamu lakukan sekarang: audit endpoint REST API di plugin yang terpasang, dan pastikan setiap permission callback memanggil current_user_can() sebelum memproses input.
Punya pengalaman menganalisis eksploitasi REST API di WordPress? Atau ada pola serangan yang belum kita bahas? Share di kolom komentar. Kita sama-sama belajar dari celah yang ditemukan di lapangan.
