D
P
0

WordPress & PHP

Domain Email Mati Lolos Jadi User WordPress? `is_email()` Cuma Cek Sintaks, Bukan Deliverability

23 Juli 2026·4 menit baca
Domain Email Mati Lolos Jadi User WordPress? `is_email()` Cuma Cek Sintaks, Bukan Deliverability

Sebuah situs klien yang saya pegang, sebuah platform booking dengan alur signup mandiri, pelan-pelan mengumpulkan akun-akun aneh. Waktu QA rutin saya buka daftar user dan menemukan satu yang bikin dahi berkerut: alamatnya testing@g.com. Domain g.com itu jelas ngaco, bukan gmail.com, dan tidak ada mail server apa pun di baliknya. Tapi akun itu bukan setengah jadi. Dia user WordPress penuh, lengkap dengan role dan meta, sudah nangkring di database entah sejak kapan.

Yang bikin makin ganjil: alur signup ini seharusnya mengirim email verifikasi dulu sebelum akun dianggap aktif. Email itu tentu tidak pernah sampai, karena domainnya mati. Tapi akun tetap dibuat lebih dulu, lalu bertahan selamanya walau verifikasinya tidak akan pernah tuntas. Database pelan-pelan kembung oleh akun-akun hantu yang mustahil diverifikasi, dan tidak ada satu proses pun yang membersihkannya.

Gejalanya

Saya coba reproduksi dengan tangan sendiri. Buka form signup, ketik domain asal-asalan: qwerty@asdf-tidak-ada.co, submit. Halaman balas sukses, minta saya cek inbox. Saya buka daftar user di wp-admin, dan benar, user baru sudah nongol di sana dengan status aktif. Email verifikasinya jelas tidak akan datang, tapi akunnya sudah telanjur ada. Saya ulangi dengan beberapa domain karangan lain, hasilnya sama persis. Setiap string yang bentuknya mirip email lolos jadi akun permanen.

Menelusuri akar masalah

Saya buka handler signup-nya dan menemukan satu-satunya gerbang validasi email: is_email(). Di situlah masalahnya. is_email() itu murni cek sintaks. Dia cocokkan string ke pola alamat yang valid menurut aturan format, lalu selesai. Dia tidak menyentuh DNS, tidak memastikan domainnya benar-benar ada, tidak mengecek apakah ada mail server yang bisa menerima pesan. Jadi apa pun yang bentuknya sah, x@y.z, testing@g.com, domain yang sudah lama mati, semuanya lulus dengan nilai penuh.

String yang lolos itu langsung dioper ke wp_create_user(). Tidak ada lapisan di antaranya. Jadi ada dua lubang yang menganga sekaligus. Pertama, gerbangnya tidak pernah bertanya "apakah email ini bisa dikirimi". Kedua, tidak ada yang membersihkan akun yang dibuat tapi tidak pernah lolos verifikasi. Lubang pertama mengizinkan sampah masuk, lubang kedua memastikan sampah itu tidak pernah keluar.

Perbaikannya

Saya tambal keduanya, dan urutannya penting: gerbang di depan untuk menahan yang jelas tidak terkirim, plus penyapu di belakang untuk yang telanjur masuk.

Lapisan pertama: cek deliverability tepat setelah is_email() lolos. Method PC_REST_Auth::validate_email_deliverability() mengambil bagian domain, lalu menanyakan DNS-nya. Kuncinya di sini adalah jangan cuma cek MX. Domain kecil atau personal kadang tidak punya record MX eksplisit, dan menurut RFC 5321 §5.1 kalau MX tidak ada, record A dipakai sebagai implicit MX. Jadi saya cek MX dulu, kalau kosong baru fallback ke A, dan hanya menolak kalau keduanya gagal.

public static function validate_email_deliverability( $email ) {
    // is_email() sudah lolos: sintaksnya benar. Sekarang cek DNS-nya.
    $domain = substr( strrchr( $email, '@' ), 1 );
 
    // Sebagian host mematikan checkdnsrr(): jangan blokir semua orang. Fail-open.
    if ( ! function_exists( 'checkdnsrr' ) ) {
        return true;
    }
 
    // MX dulu. Tidak ada MX? RFC 5321 §5.1: pakai record A (implicit MX).
    if ( ! checkdnsrr( $domain, 'MX' ) && ! checkdnsrr( $domain, 'A' ) ) {
        return new WP_Error(
            'rest_undeliverable_email',
            'Domain email tidak punya mail server yang bisa dihubungi.',
            array( 'status' => 400 )
        );
    }
 
    return true;
}

Perhatikan cabang function_exists. Sebagian managed host menonaktifkan checkdnsrr(), dan kalau saya keras menolak setiap kali fungsi itu tidak ada, saya justru mematikan signup untuk semua orang di host semacam itu. Jadi saya soft-pass alias fail-open: kalau alat ceknya tidak tersedia, biarkan lewat, jangan bikin regresi yang lebih parah dari bug aslinya.

Menyambungkannya ke handler cukup satu blok, dan yang penting ia berjalan sebelum wp_create_user() pernah dipanggil:

$deliverable = self::validate_email_deliverability( $email );
if ( is_wp_error( $deliverable ) ) {
    return $deliverable; // hentikan sebelum akun sempat dibuat
}

Lapisan kedua: cron harian pc_purge_unverified_users untuk menyapu akun yang telanjur ada dan tidak akan pernah verified. Aturannya konservatif supaya tidak ada yang kehapus tanpa sengaja. Hapus hanya akun yang belum terverifikasi lebih dari 7 hari, yang bukan admin, bukan seed demo, dan tidak memiliki satu pun post pc_property.

add_action( 'pc_purge_unverified_users', function () {
    $stale = get_users( array(
        'meta_key'   => 'pc_email_verified',
        'meta_value' => '0',
        'date_query' => array(
            array( 'before' => '7 days ago', 'column' => 'user_registered' ),
        ),
    ) );
 
    foreach ( $stale as $user ) {
        if ( user_can( $user, 'manage_options' ) )            continue; // admin, jangan sentuh
        if ( get_user_meta( $user->ID, 'pc_demo_seed', true ) ) continue; // seed demo, biarkan
        if ( count_user_posts( $user->ID, 'pc_property' ) > 0 ) continue; // punya properti, biarkan
 
        require_once ABSPATH . 'wp-admin/includes/user.php';
        wp_delete_user( $user->ID );
    }
} );

Tiga syarat itu adalah rem pengaman. Punya pc_property berarti akun ini benar dipakai, apa pun status emailnya. Seed demo dilindungi supaya tidak ikut tersapu. Dan admin jelas tidak boleh terhapus oleh proses otomatis mana pun.

Checklist

  • is_email() cuma cek sintaks. Lolos di situ tidak berarti emailnya bisa dikirimi apa pun.
  • Setelah sintaks lolos, cek deliverability domain lewat checkdnsrr($domain, 'MX').
  • Fallback ke record A sesuai RFC 5321 §5.1 supaya domain tanpa MX eksplisit tetap valid.
  • Fail-open kalau checkdnsrr() dimatikan host: jangan matikan signup semua orang demi menahan yang ngaco.
  • Jalankan gerbang deliverability sebelum wp_create_user(), bukan sesudah akun telanjur dibuat.
  • Pasang cron pembersih untuk akun unverified yang lama, dengan syarat konservatif: bukan admin, bukan seed, tanpa konten milik sendiri.