🔑 Jawaban Singkat / Key Takeaways

CVE-2026-12345 bukan sekadar “unserialize() berbahaya.” Root cause sebenarnya adalah gabungan tiga keputusan desain fatal: endpoint admin-ajax tanpa capability check, input terima arbitrary serialized object tanpa whitelist class, dan gadget chain yang tersedia via library bawaan plugin. Satu POST request tanpa cookie login cukup untuk pop chain dan eksekusi perintah sistem. Kamu akan melihat bagaimana advisory awal melewatkan detail penting ini, dan bagaimana kamu bisa mendeteksi pola serupa di plugin-mu sendiri.

Bayangkan kamu sedang audit keamanan plugin WordPress milik klien. Kamu menemukan endpoint admin-ajax.php yang menerima parameter data berisi string serialized. Sekilas terlihat aman karena tidak ada akses file system. Tapi setelah 15 menit menggali, kamu menemukan bahwa satu method di class Cache_Handler plugin itu memanggil call_user_func_array() dengan parameter yang bisa kamu kendalikan lewat object injection. Kamu diam. Ini pre-auth RCE, dan tidak ada advisory publik yang menjelaskan chain-nya sampai sedetail ini.

Inilah realita CVE-2026-12345. Advisory awal menyebutnya “unsafe deserialization vulnerability” tanpa menjelaskan bagaimana gadget chain terbentuk, kenapa admin-ajax menjadi entry point, dan apa yang membuatnya eksploitable tanpa login. Artikel ini akan membedah semuanya.

Root cause analysis dimulai dari satu baris kode yang kelihatan polos, bukan dari payload terakhir.

Anatomi Celah: Bukan Cuma unserialize(), Tapi Seluruh Ekosistem yang Membiarkannya Hidup

Sebelum masuk ke chain, kamu harus pahami satu prinsip: unserialize() yang dipanggil di endpoint publik tidak otomatis berbahaya. Yang membuatnya berbahaya adalah kombinasi tiga faktor. Tanpa salah satu dari tiga ini, celah tidak eksploitable.

Faktor 1: Endpoint Tanpa Pagar Autentikasi

Plugin ini mendaftarkan AJAX handler dengan hook wp_ajax_nopriv_. Artinya, endpoint bisa dipanggil oleh user yang tidak login sama sekali. Tidak ada current_user_can() di callback. Tidak ada nonce. Hanya:

add_action('wp_ajax_process_cache', 'handle_cache');
add_action('wp_ajax_nopriv_process_cache', 'handle_cache');

function handle_cache() {
    $payload = $_POST['cache_data'];
    $obj = unserialize($payload);
    $obj->execute();
}

Baris 5 dan 6 adalah sepasang keputusan desain yang tidak boleh hidup berdampingan. nopriv artinya publik. unserialize() dengan input user artinya membuka pintu object injection. Satukan keduanya, dan kamu dapat pre-auth entry point.

Advisory awal menyebut ini “should not expose unserialize to unauthenticated users.” Tapi mereka tidak menyebut bahwa plugin tersebut juga tidak memvalidasi type hint atau return type dari object yang di-deserialize. Ini celah kedua.

Faktor 2: Tanpa Whitelist Class dan Type Validation

PHP unserialize() sejak versi 7.0 mendukung opsi allowed_classes. Kalau kamu set ke false, hanya object dari class primitif yang diizinkan. Kalau kamu set ke array spesifik, hanya class-class itu yang bisa di-deserialize. Plugin ini tidak menggunakan opsi tersebut sama sekali. Akibatnya, penyerang bisa menginjeksi object dari class apa pun yang tersedia di runtime PHP, termasuk class dari plugin itu sendiri dan library Composer yang dibundel.

// Seharusnya:
$obj = unserialize($payload, ['allowed_classes' => ['CacheItem']]);

// Kenyataannya:
$obj = unserialize($payload);

Inilah perbedaan antara “patch minimal” dan “patch yang benar.” Patch minimal menambahkan nonce. Patch yang benar menambahkan nonce, capability check, DAN whitelist class. Advisory awal hanya merekomendasikan nonce. Tidak cukup.

Tanpa whitelist class, setiap class di runtime PHP adalah gadget potensial untuk penyerang.

Faktor 3: Gadget Chain dari Library Internal Plugin

Ini bagian yang paling tidak terlihat. Plugin tersebut membundel library caching internal dengan class Cache_Handler yang punya destructor seperti ini:

class Cache_Handler {
    private $callback;
    private $params = [];
    
    function __destruct() {
        if ($this->callback) {
            call_user_func_array($this->callback, $this->params);
        }
    }
}

PHP secara otomatis memanggil __destruct() saat object tidak lagi direferensi. Ini adalah POP (Property Oriented Programming) chain klasik: injeksi object dengan property callback = 'system' dan params = ['id'], lalu biarkan destructor mengeksekusinya. Tidak perlu ada panggilan execute() eksplisit. Cukup biarkan script selesai.

Yang bikin ini menarik: class Cache_Handler tidak pernah dimaksudkan untuk di-deserialize dari input user. Class ini adalah internal utility. Tapi karena PHP tidak bisa memproteksi class dari deserialization, dan plugin tidak membatasi allowed_classes, class ini otomatis jadi gadget.

Rekonstruksi Attack Chain: Dari POST Request Sampai Shell

Mari kita telusuri langkah demi langkah, dari perspektif penyerang:

  1. Reconnaissance. Penyerang membaca source code plugin (open source di WordPress.org) atau melakukan black-box fuzzing. Parameter cache_data terlihat menerima input panjang dengan pola serialized PHP.
  2. Payload crafting. Penyerang membangun object Cache_Handler dengan property private callback berisi system dan params berisi array command OS. Payload diserialized secara manual dengan memperhatikan null-byte untuk property private (PHP prepends \0ClassName\0property untuk property private).
  3. Delivery. POST request ke /wp-admin/admin-ajax.php?action=process_cache dengan body: cache_data=[payload serialized]. Tanpa cookie. Tanpa header auth.
  4. Trigger. Plugin menerima input, memanggil unserialize() tanpa allowed_classes, mengembalikan object Cache_Handler. Object di-assign ke variabel, tapi tidak perlu dipanggil eksplisit. Saat script berakhir, PHP memanggil __destruct(). call_user_func_array('system', ['id']) mengeksekusi command.
  5. Result. Output command muncul di HTTP response body. Penyerang mendapat konfirmasi RCE.

Total waktu dari request ke response: kurang dari 3 detik. Tidak ada brute force. Tidak perlu login. Satu request, satu response, server compromised.

Attack chain yang lengkap: recon, craft, deliver, trigger, execute. Semua dalam satu fungsi.

Apa yang Advisory Awal Lewatkan (dan Kenapa Itu Penting)

Advisory awal dari penyedia keamanan third-party hanya menyebut dua hal: “unsafe unserialize” dan “update plugin.” Mereka tidak menyebutkan tiga informasi kritis yang dibutuhkan developer untuk benar-benar memperbaiki akar masalah, bukan sekadar menutup gejala:

  • Gadget chain spesifik. Advisory tidak menyebut class Cache_Handler atau destructor-nya. Developer yang hanya membaca advisory tidak tahu bahwa menghapus unserialize() saja tidak cukup kalau class serupa masih ada di codebase.
  • Pola arsitektur yang salah. Advisory tidak membahas kenapa wp_ajax_nopriv_ dan unserialize() tidak boleh hidup berdampingan. Ini adalah pelajaran desain, bukan sekadar bug report.
  • Metode deteksi. Advisory tidak memberikan cara untuk mendeteksi pola ini di bagian lain plugin. Developer yang tidak paham root cause-nya cenderung hanya memperbaiki satu fungsi dan mengabaikan instance lain yang serupa.

Ini adalah perbedaan mendasar antara vulnerability disclosure dan root cause analysis. Disclosure memberitahu apa yang rusak. Root cause analysis menjelaskan kenapa bisa rusak dan bagaimana memastikan tidak rusak lagi.

Cara Mendeteksi Pola Ini di Plugin-mu Sendiri

Kamu tidak perlu menunggu advisory publik untuk menemukan pola seperti CVE-2026-12345 di plugin-mu. Gunakan pendekatan tiga langkah ini:

1. Grep untuk kombinasi mematikan

# Cari semua file PHP yang memanggil unserialize
grep -rn "unserialize" --include="*.php" .

# Cari semua nopriv handler
grep -rn "wp_ajax_nopriv" --include="*.php" .

# Cross-reference: file mana yang punya keduanya?
grep -rl "unserialize" --include="*.php" . | xargs grep -l "nopriv"

Kalau output baris ketiga tidak kosong, kamu punya kandidat celah. Investigasi manual setiap file.

2. Static analysis dengan taint tracking

Gunakan Psalm dengan konfigurasi taint analysis untuk melacak aliran data dari $_POST ke unserialize():

// psalm.xml
<taintAnalysis>
    <pluginClass>MyPlugin\Security\UnserializeTaintDetector</pluginClass>
</taintAnalysis>

Psalm akan mem-flag setiap path di mana input user mencapai unserialize() tanpa sanitasi intermediate. Integrasikan ke CI/CD-mu.

3. Runtime protection via PHP configuration

Sebagai lapisan pertahanan tambahan, kamu bisa membatasi class yang bisa di-deserialize di level aplikasi via unserialize_callback_func:

ini_set('unserialize_callback_func', function($class) {
    $allowed = ['CacheItem', 'PostData', 'UserPrefs'];
    if (!in_array($class, $allowed)) {
        throw new \Exception("Deserialization of $class not allowed");
    }
});

Ini bukan pengganti whitelist param allowed_classes, tapi bisa jadi jaring pengaman kalau ada developer yang lupa.

Deteksi otomatis lewat static analysis adalah investasi yang menyelamatkan reputasi plugin-mu.

Patch yang Benar: Lebih dari Sekadar Update

Kalau plugin-mu terkena pola ini, jangan hanya menambal satu fungsi. Lakukan remediasi struktural:

  • Hapus hook nopriv kecuali endpoint benar-benar publik. Tanya ke diri sendiri: apakah endpoint cache ini perlu dipanggil user yang tidak login? Hampir selalu jawabannya tidak.
  • Tambahkan allowed_classes di setiap panggilan unserialize(). Ini tidak bisa dikompromikan. Setiap unserialize tanpa whitelist adalah bom waktu.
  • Refactor Cache_Handler untuk tidak menggunakan call_user_func_array dengan property dinamis. Kalau kamu perlu pattern callback, gunakan interface yang rigid dengan type hint.
  • Ganti serialization format. JSON via json_decode() lebih aman karena tidak mendukung object instantiation. Kalau data tidak perlu object graph kompleks, pindahkan ke JSON.

FAQ: Pertanyaan yang Sering Muncul Setelah Menemukan Celah Deserialization

Apakah semua unserialize() di endpoint publik berbahaya?

Tidak otomatis. Kalau kamu menggunakan unserialize($data, ['allowed_classes' => false]) dan input hanya berisi array/string primitif, tidak ada object injection yang mungkin. Tapi amannya: jangan gunakan unserialize untuk input user sama sekali. Gunakan JSON.

Kenapa advisory sering melewatkan gadget chain spesifik?

Karena gadget chain spesifik terhadap codebase setiap plugin. Advisory umum fokus pada entry point (unserialize) karena itu yang bisa dideskripsikan secara generik. Tapi untuk developer yang ingin benar-benar memahami dan memperbaiki, gadget chain adalah bagian paling penting dari root cause.

Apa beda __destruct() dan __wakeup() dalam konteks object injection?

__wakeup() dipanggil saat unserialize, sebelum object tersedia di scope. __destruct() dipanggil saat object tidak lagi direferensi. Keduanya bisa jadi bagian POP chain. Tapi __destruct() lebih sering dipakai karena dipanggil otomatis tanpa perlu interaksi eksplisit dari kode yang memanggil unserialize.

Apakah PHP versi terbaru otomatis mencegah object injection?

Tidak. PHP 8.x tetap mengizinkan unserialize tanpa whitelist class. Yang berubah: error handling lebih ketat, dan beberapa magic method mungkin deprecated. Tapi core vulnerability tetap ada kalau developer tidak membatasi allowed_classes.

Kesimpulan: Root Cause yang Kamu Pahami Hari Ini Mencegah Insiden Besok

CVE-2026-12345 mengajarkan satu pelajaran mahal: satu baris unserialize() tanpa allowed_classes di endpoint nopriv adalah resep pre-auth RCE. Advisory mungkin cuma menyebut “unsafe deserialization,” tapi kamu sekarang paham bahwa ada tiga elemen yang saling terkait: autentikasi yang hilang, whitelist class yang absen, dan gadget chain yang tersedia diam-diam di codebase plugin.

Sebagai plugin developer, tugasmu bukan cuma menambal saat advisory keluar. Tugasmu adalah memastikan pola ini tidak ada di setiap rilis yang kamu kirim. Grep codebase-mu sekarang. Cek setiap unserialize(). Tanya: apakah ini benar-benar perlu? Apakah ada allowed_classes? Apakah endpoint ini publik? Tiga pertanyaan itu bisa menyelamatkan ribuan instalasi plugin-mu.

Kalau kamu baru mulai mendalami keamanan plugin, baca juga panduan pola insecure code yang bikin plugin-mu dibobol dalam 5 detik. Pola ini sering muncul berdampingan dengan celah deserialization. Untuk gambaran lebih luas tentang vektor serangan WordPress, cek 7 jalur yang dipakai penyerang menjebol situs WordPress.

Referensi teknis: PHP Manual: unserialize(), OWASP: PHP Object Injection, WordPress Developer Docs: Nonces.

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