⚡ Jawaban Singkat / Key Takeaways

Dua kesalahan kecil; missing capability check dan absent nonce verification; bisa mengubah endpoint AJAX biasa menjadi gerbang privilege escalation. Pola ini muncul berulang di puluhan plugin WordPress populer, dan penyerang hanya butuh satu HTTP request tanpa autentikasi untuk mengeksekusi fungsi administratif.

Kenapa Plugin Security Lebih Rapuh dari yang Kamu Kira

Kamu bangun jam 3 pagi. Notifikasi Slack berbunyi: “Admin user baru muncul di wp_users. Bukan kita yang bikin.” Setelah investigasi dua jam, kamu nemu sumbernya: endpoint AJAX di plugin custom-mu sendiri. Tidak ada password. Tidak ada session. Cukup satu POST request.

Ini bukan skenario fiksi. Pola ini nyata dan berulang. Patchstack melaporkan 4.832 kerentanan plugin pada 2025, dan 58% di antaranya terkait missing atau broken authorization. Angka ini bukan kebetulan. Ini pola.

Root cause analysis dimulai dari membaca kode. Bukan dari panic response.

Anatomi Pola Insecure Code: Apa yang Sebenarnya Terjadi

Mayoritas plugin developer menulis endpoint seperti ini tanpa sadar bahwa mereka sedang membangun celah:

add_action('wp_ajax_update_user_role', 'handle_update_role');
add_action('wp_ajax_nopriv_update_user_role', 'handle_update_role');

function handle_update_role() {
    $user_id = $_POST['user_id'];
    $new_role = $_POST['role'];
    wp_update_user(array('ID' => $user_id, 'role' => $new_role));
    wp_send_json_success();
}

Sekilas terlihat normal. AJAX handler. Input user_id dan role. Update user. Kirim response. Tapi tiga hal fatal hilang: validasi capability, verifikasi nonce, dan sanitasi input. Penyerang cukup kirim POST request dengan user_id=1&role=administrator, dan selesai. Site takeover.

Yang bikin pola ini berbahaya bukan kerumitannya; justru karena terlalu sederhana. Developer mengira WordPress core otomatis memproteksi endpoint AJAX. Tidak. Core hanya menyediakan tools-nya. Kamu harus pakai.

Missing Capability Check: Gerbang Pertama yang Selalu Lupa Dikunci

current_user_can() adalah fungsi sebaris, tapi ketiadaannya setara dengan membuka pintu admin ke publik. Saat endpoint tidak mengecek capability, siapa pun yang punya akun WordPress; bahkan subscriber; bisa mengeksekusi fungsi yang seharusnya hanya untuk administrator.

Pattern yang benar:

function handle_update_role() {
    if (!current_user_can('edit_users')) {
        wp_send_json_error('Unauthorized', 403);
    }
    // ... logic
}

Tapi satu baris ini sering hilang. Alasannya? Developer testing pakai akun admin, jadi tidak pernah lihat error 403. Masalah muncul saat plugin dirilis ke production dan user dengan role rendah menemukan endpoint tersebut via source code atau REST API enumeration.

Khusus endpoint REST API (register_rest_route), gunakan permission_callback:

register_rest_route('myplugin/v1', '/update-role', array(
    'methods' => 'POST',
    'callback' => 'handle_update_role',
    'permission_callback' => function() {
        return current_user_can('edit_users');
    },
));

Tanpa permission_callback, WordPress tidak otomatis memblokir request. Sama seperti AJAX handler; kamu yang bertanggung jawab.

Code review fokus keamanan harus jadi kebiasaan sebelum setiap release.

Absent Nonce Verification: Kenapa Tanpa Nonce, Request-mu Bisa Dipalsukan

Capability check melindungi dari unauthorized access. Nonce melindungi dari CSRF (Cross-Site Request Forgery). Dua lapisan yang berbeda, dua-duanya wajib ada.

Skenario nyata: admin yang sah sedang login. Dia buka tab browser lain yang berisi halaman jahat. Halaman itu mengirim POST request tersembunyi ke endpoint plugin-mu. Karena admin punya capability, request lolos. Tapi request itu bukan berasal dari dashboard WordPress.

Nonce mencegah ini. Implementasi minimal:

function handle_update_role() {
    check_ajax_referer('myplugin_update_role', 'nonce');
    // ... capability check + logic
}

Dan di JavaScript-mu:

const data = {
    action: 'update_user_role',
    user_id: userId,
    role: newRole,
    nonce: myplugin_ajax.nonce, // dari wp_localize_script
};

Pola yang sering meleset: developer bikin nonce tapi tidak mem-verify di server-side. Atau bikin nonce tapi tidak mengikatnya ke action spesifik. Nonce WordPress harus spesifik per action; wp_create_nonce('myplugin_update_role'). Jangan pakai nonce generik untuk semua endpoint.

Pola Tersembunyi yang Lolos Code Review

Beberapa developer sudah memasang capability check dan nonce. Tapi celah tetap ada. Kenapa? Karena pola salah pakai:

  • Nonce di-check tapi tidak di-return sebagai error fatal. wp_verify_nonce() mengembalikan false, tapi kode lanjut eksekusi karena tidak ada early return.
  • Capability dicek pakai string hard-coded, bukan capability yang tepat. Misal current_user_can('administrator') alih-alih current_user_can('manage_options'). Yang pertama bukan capability valid; selalu return false.
  • Timing check. Capability dicek setelah operasi database dilakukan. Kalau operasi gagal di tengah, data sudah berubah separuh.
  • Self-request nonce issue. Nonce dibuat di halaman publik; penyerang bisa ambil nonce dari source code halaman dan pakai untuk CSRF.

Checklist Verifikasi Sebelum Release Plugin

Simpan daftar ini sebagai pre-release ritual-mu. Cek satu per satu, setiap kali deploy:

  • ✅ Setiap wp_ajax_* punya current_user_can() di baris pertama callback
  • ✅ Setiap wp_ajax_nopriv_* sudah dievaluasi ulang; apakah benar perlu diakses publik?
  • ✅ Setiap AJAX/REST endpoint punya check_ajax_referer() atau permission_callback
  • ✅ Capability string valid: edit_users, manage_options, edit_posts; bukan role name
  • ✅ Nonce di-generate per-action spesifik, bukan satu nonce untuk semua endpoint
  • ✅ Semua input ($_POST, $_GET, $_REQUEST) melewati sanitize_text_field(), intval(), atau sanitizer yang sesuai
  • ✅ Early return (wp_send_json_error() + wp_die()) aktif saat validasi gagal
  • add_action tidak dipanggil sebelum wp_loaded (hindari timing exploit)
Satu baris current_user_can() bisa selamatkan 50.000 instalasi plugin-mu.

Tooling: Otomatiskan Deteksi Sebelum Penyerang Mendeteksinya

Manual checklist bagus, tapi manusia lupa. Gunakan tool:

  • PHPStan + WordPress stubs. Rule phpstan.neon custom bisa deteksi missing check_ajax_referer pada setiap AJAX handler.
  • RIPS / Psalm. Static analysis untuk taint tracking; deteksi input dari $_POST yang mengalir ke wp_update_user() tanpa sanitasi.
  • Plugin Check (WordPress Meta). Plugin milik tim WordPress.org yang mengaudit plugin sebelum submission. Gunakan sebelum submit.
  • Automated test untuk endpoint. Tulis PHPUnit test yang mengirim POST request tanpa nonce dan pastikan response 403.

Jangan tunggu penyerang yang menemukan celah di plugin-mu. Integrasikan static analysis ke CI/CD pipeline-mu. Satu commit tanpa capability check harus memblokir merge request.

Static analysis tools adalah teman terbaik plugin developer yang serius soal keamanan.

FAQ: Pertanyaan yang Sering Muncul Setelah Post-Mortem

Kenapa WordPress nggak otomatis memblokir AJAX endpoint tanpa capability check?

Karena WordPress bersifat extensible. Core tidak tahu apakah endpoint-mu memang sengaja publik (seperti login, register, search) atau privat. Fleksibilitas ini sengaja diberikan, tapi tanggung jawab proteksi ada di plugin developer.

Apa beda wp_die() dengan wp_send_json_error() setelah validasi gagal?

wp_send_json_error() mengirim JSON response dan memanggil wp_die() secara internal. Pakai wp_send_json_error() untuk endpoint yang dipanggil via JavaScript. Pakai wp_die() manual hanya jika kamu butuh custom error message HTML. Yang penting: eksekusi harus berhenti; jangan lanjut ke operasi database.

Apakah nonce cukup untuk mencegah privilege escalation?

Tidak. Nonce hanya mencegah CSRF. Nonce tidak memverifikasi siapa yang mengirim request atau apa yang diizinkan. Kombinasi nonce + capability check wajib. Dua-duanya, bukan salah satu.

Bagaimana kalau plugin-ku sudah terlanjur dirilis tanpa nonce?

Rilis minor update secepatnya. Tambahkan nonce verification di server-side, update JavaScript handler-mu untuk mengirim nonce. Gunakan wp_localize_script() untuk passing nonce ke frontend. Komunikasikan changelog dengan jelas: “Security hardening; adds CSRF protection.” Jangan tunda.

Kesimpulan

Privilege escalation tidak butuh exploit kit canggih. Cukup satu endpoint tanpa current_user_can() dan tanpa check_ajax_referer(). Dua baris kode yang hilang, ribuan situs terdampak.

Kamu sebagai plugin developer memegang kunci situs pengguna. Jangan sia-siakan kepercayaan itu. Jadikan capability check dan nonce verification sebagai reflek, bukan afterthought. Kalau kamu masih menggunakan template boilerplate yang tidak mencakup dua lapisan ini, sekarang saatnya rewrite.

Baca juga: Studi kasus input sanitization flaw di REST API plugin populer. Dan kalau kamu penasaran bagaimana AI coding tools bisa bantu deteksi celah ini sebelum deploy, cek panduan AI coding agents untuk WordPress development.

Sumber referensi: WordPress Nonce Documentation, WordPress Roles & Capabilities, OWASP Top 10 – Broken Access Control.

About the Author

Dzul Qurnain

Suka nonton Anime, ngoding dan bagi-bagi tips kalau tahu.. Oh iya, suka baca ( tapi yang menarik menurutku aja)... Praktisi WordPress, web development, SEO, dan server administration yang membagikan tutorial teknis dan catatan implementasi nyata.

View All Articles