Wordpress Security Best Practices Nonces Capabilities Escaping Sanitization Safe Output
Definisi singkat
Di WordPress, keamanan input-output tidak diselesaikan oleh satu fungsi tunggal. Kamu perlu memisahkan beberapa lapisan:
- capabilities untuk memastikan user memang berwenang melakukan aksi
- nonces untuk mengurangi risiko CSRF pada request yang berasal dari browser
- sanitization untuk membersihkan data sebelum disimpan atau diproses
- escaping untuk mengamankan data saat ditampilkan ke HTML, attribute, URL, JS, atau konteks lain
- safe output untuk memastikan admin UI dan frontend tidak memantulkan data mentah
Kalau kelimanya dipakai dengan benar, plugin/theme biasanya jauh lebih tahan terhadap input berbahaya, request palsu, dan output yang bocor ke halaman.
Kapan memakai masing-masing mekanisme
1) Capabilities
Pakai capabilities saat kamu perlu menjawab pertanyaan: “Siapa yang boleh melakukan aksi ini?”
Contoh:
- membuka halaman admin tertentu
- menyimpan pengaturan plugin
- menghapus data custom
- mengubah post meta tertentu
- menjalankan aksi sensitif dari AJAX atau custom endpoint
Capabilities dicek dengan fungsi seperti current_user_can().
if ( ! current_user_can('manage_options') ) {
wp_die('You are not allowed to access this page.');
}Capability itu bukan sekadar tampilan menu. Menu bisa disembunyikan, tapi callback, endpoint, dan save handler tetap harus dicek lagi.
2) Nonces
Pakai nonces saat request berasal dari browser dan berpotensi dipalsukan oleh situs lain, terutama untuk:
- form admin
- aksi AJAX
- custom action di frontend
- link/action yang mengubah state
Nonce WordPress dipakai untuk memverifikasi bahwa request memang berasal dari konteks yang kamu harapkan, bukan submit acak dari luar.
Contoh form:
wp_nonce_field('myplugin_save_settings', 'myplugin_nonce');Contoh verifikasi:
if ( ! isset($_POST['myplugin_nonce']) ||
! wp_verify_nonce($_POST['myplugin_nonce'], 'myplugin_save_settings') ) {
wp_die('Invalid request.');
}Nonce bukan pengganti capability. Nonce menjawab “apakah request ini valid dari browser yang diharapkan?”, bukan “apakah user berhak?”.
3) Sanitization
Pakai sanitization saat data masuk ke sistem dan akan disimpan atau diproses lebih lanjut.
Contoh:
- text field →
sanitize_text_field() - textarea ringan →
sanitize_textarea_field() - email →
sanitize_email() - URL →
esc_url_raw()atausanitize_url()tergantung konteks dan versi WordPress - HTML terbatas →
wp_kses_post()atauwp_kses()dengan allowlist - slug / key / class name → sanitization khusus yang sesuai format
Sanitization mencegah data kotor masuk ke database, option, meta, atau transient.
4) Escaping
Pakai escaping saat data keluar ke template, admin page, email HTML, attribute, inline JS, atau URL.
Contoh umum:
- HTML text node →
esc_html() - attribute →
esc_attr() - URL →
esc_url() - textarea output →
esc_textarea() - JSON inline / script context →
wp_json_encode()dan pola output yang aman
Aturan singkatnya: sanitize on input, escape on output.
5) Safe admin/frontend output
Pakai safe output ketika data dari database, API, user input, atau metadata ditampilkan di:
- admin table
- settings page
- metabox
- custom dashboard widget
- frontend shortcode
- block render callback
- template theme
Jangan pernah menganggap data dari database sudah aman hanya karena pernah disimpan. Data tersimpan tetap harus di-escape sesuai konteks render.
Alur kerja yang sehat
Urutan yang paling aman biasanya begini:
- request masuk dari form, AJAX, atau endpoint
- capability dicek lebih dulu
- nonce diverifikasi jika request berbasis browser state-changing
- input disanitasi sesuai bentuk datanya
- data disimpan ke database/option/meta
- data dibaca lagi saat render
- output di-escape sesuai konteks HTML/attribute/URL/JS
Diagram alur
Cara melindungi action dan form
Form admin
Untuk form admin yang menyimpan state, gunakan tiga lapisan sekaligus:
- capability check di awal render dan sebelum save
- nonce field di form
- sanitization callback atau sanitization manual sebelum save
Contoh pola:
function myplugin_render_page() {
if ( ! current_user_can('manage_options') ) {
wp_die('You are not allowed to access this page.');
}
?>
<form method="post" action="">
<?php wp_nonce_field('myplugin_save_settings', 'myplugin_nonce'); ?>
<label for="myplugin_title">Title</label>
<input
type="text"
id="myplugin_title"
name="myplugin_title"
value="<?php echo esc_attr(get_option('myplugin_title', '')); ?>"
>
<?php submit_button('Save Changes'); ?>
</form>
<?php
}Lalu saat submit:
function myplugin_handle_save() {
if ( ! current_user_can('manage_options') ) {
wp_die('You are not allowed to perform this action.');
}
if ( ! isset($_POST['myplugin_nonce']) ||
! wp_verify_nonce($_POST['myplugin_nonce'], 'myplugin_save_settings') ) {
wp_die('Invalid request.');
}
$title = isset($_POST['myplugin_title'])
? sanitize_text_field(wp_unslash($_POST['myplugin_title']))
: '';
update_option('myplugin_title', $title);
}Action link atau destructive action
Kalau aksi mengubah data lewat link atau button, nonce tetap perlu. Biasanya bentuknya seperti:
$url = wp_nonce_url(
admin_url('admin-post.php?action=myplugin_delete_cache'),
'myplugin_delete_cache'
);Di handler:
if ( ! current_user_can('manage_options') ) {
wp_die('Forbidden');
}
check_admin_referer('myplugin_delete_cache');AJAX
Untuk AJAX, pola amannya serupa:
- cek capability di callback
- verifikasi nonce dengan
check_ajax_referer() - sanitasi payload
- kembalikan response terstruktur
add_action('wp_ajax_myplugin_save', function () {
if ( ! current_user_can('edit_posts') ) {
wp_send_json_error(['message' => 'Forbidden'], 403);
}
check_ajax_referer('myplugin_save', 'nonce');
$value = isset($_POST['value'])
? sanitize_text_field(wp_unslash($_POST['value']))
: '';
wp_send_json_success(['value' => $value]);
});Cara melindungi output admin
Admin UI sering terasa “aman” karena hanya bisa diakses user login, padahal tetap rawan XSS dari data yang tersimpan.
Prinsip utama
- text di HTML →
esc_html() - value input →
esc_attr() - URL tombol/link →
esc_url() - textarea →
esc_textarea() - HTML yang memang diizinkan →
wp_kses_post()atauwp_kses()
Contoh:
echo '<h1>' . esc_html($page_title) . '</h1>';
echo '<a href="' . esc_url($settings_url) . '">Settings</a>';
echo '<input value="' . esc_attr($saved_value) . '">';Jebakan umum di admin
- menampilkan
$_POSTlangsung setelah submit - memakai
echo $optiontanpa escape karena datanya “dari admin” - mencampur HTML dan PHP tanpa sadar konteks outputnya
- menggunakan
wp_kses_post()untuk semua output padahal kadang terlalu longgar - lupa escape
selected(),checked(),disabled()jika dipakai dalam markup custom
Cara melindungi output frontend
Frontend sering lebih berbahaya karena user publik bisa melihat hasilnya langsung.
Kasus umum
- shortcode yang menerima atribut user
- blok dinamis yang render HTML dari data CMS
- template theme yang menampilkan custom field
- daftar komentar, profil, atau data import
Contoh shortcode:
function myplugin_box_shortcode($atts) {
$atts = shortcode_atts([
'title' => '',
], $atts, 'myplugin_box');
$title = sanitize_text_field($atts['title']);
return '<div class="box"><h3>' . esc_html($title) . '</h3></div>';
}
add_shortcode('myplugin_box', 'myplugin_box_shortcode');Catatan penting: atribut shortcode boleh disanitasi saat diproses, tetapi tetap di-escape saat dirender.
Sanitization vs escaping: bedanya jangan ketukar
Ini salah satu jebakan paling umum.
Sanitization
Dipakai untuk membersihkan data sebelum disimpan atau diproses.
Tujuan:
- mengurangi karakter berbahaya atau tidak valid
- menormalkan format
- mencegah data buruk masuk ke storage
Escaping
Dipakai untuk mengamankan data ketika ditampilkan.
Tujuan:
- mencegah browser menafsirkan data sebagai HTML, attribute, script, atau URL aktif
- memastikan output sesuai konteks render
Aturan praktis
- simpan data yang sudah disanitasi
- render data yang di-escape
- jangan berharap sanitization saja cukup untuk melindungi output
- jangan escape data terlalu awal lalu simpan hasil escape ke database, kecuali memang itu bentuk final yang disengaja
Jebakan umum saat membangun plugin/theme
1) Mengira nonce = authorization
Nonce bukan izin. User tetap harus lolos capability check.
2) Lupa wp_unslash() saat baca $_POST
WordPress menambahkan slashing pada superglobals. Data input biasanya perlu di-wp_unslash() sebelum disanitasi.
$value = sanitize_text_field(wp_unslash($_POST['value'] ?? ''));3) Escape di tempat yang salah
- sanitize sebelum save
- escape saat output
- jangan dibalik tanpa alasan khusus
4) Memakai helper escape/sanitize yang tidak cocok konteks
Contoh:
- URL ditangani seperti text biasa
- HTML content disimpan dengan
sanitize_text_field()sehingga markup yang memang diizinkan ikut hilang - attribute dirender tanpa
esc_attr()
5) Trusting data dari database
Data lama, data migrasi, dan data impor bisa saja kotor. Database bukan zona aman otomatis.
6) Menulis callback render yang langsung echo raw data
Template dan admin page sering bocor XSS karena developer fokus pada layout, bukan konteks output.
7) Mengabaikan konteks output berbeda
Satu nilai bisa butuh escaping yang berbeda tergantung tempatnya:
- di text node
- di attribute
- di URL
- di JavaScript
- di HTML yang di-allowlist
8) Memberi capability terlalu longgar
Kalau admin-only action diberi capability yang terlalu umum, permukaan risiko jadi melebar.
Pilih capability yang sesuai dengan dampak aksinya, bukan sekadar yang paling gampang bekerja.
Contoh pola plugin yang aman
Pola ringkas yang sehat untuk banyak kasus plugin/theme:
function myplugin_save_setting() {
if ( ! current_user_can('manage_options') ) {
wp_die('Forbidden');
}
check_admin_referer('myplugin_save_setting');
$title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
update_option('myplugin_title', $title);
}
function myplugin_render_setting() {
$title = get_option('myplugin_title', '');
echo '<input type="text" name="title" value="' . esc_attr($title) . '">';
}Intinya:
- cek otorisasi dulu
- validasi request dengan nonce jika perlu
- bersihkan input sebelum simpan
- escape output sesuai konteks
Ringkasan cepat
- Capabilities dipakai untuk izin akses dan izin aksi.
- Nonces dipakai untuk memverifikasi request browser dan mencegah CSRF pada aksi state-changing.
- Sanitization dipakai saat data masuk atau sebelum disimpan.
- Escaping dipakai saat data keluar ke HTML, attribute, URL, atau konteks lain.
- Safe output berarti admin/frontend tidak pernah memantulkan data mentah tanpa konteks yang jelas.
Kalau kamu membangun plugin/theme WordPress, biasakan satu pertanyaan untuk tiap layer: siapa yang boleh, request ini valid atau tidak, datanya aman disimpan atau tidak, dan output ini harus di-escape dengan cara apa.