Note Permissions dan Security
Ringkasan
Video ini membahas cara mengamankan fitur Notes. Kita menambahkan custom capability type pada Note CPT, mengatur izin per-role menggunakan Members plugin, memaksa semua note berstatus private melalui wp_insert_post_data filter, dan memasang sanitasi server-side untuk mencegah injeksi kode berbahaya.
1. Capability Type untuk Note CPT
Update Registrasi CPT
Di mu-plugins/university-post-types.php:
register_post_type('note', array(
'capability_type' => 'note', // Buat permission unik untuk note
'map_meta_cap' => true, // WordPress auto-mapping ke capabilities
'show_in_rest' => true,
'supports' => array('title', 'editor'),
'public' => false,
'show_ui' => true,
'labels' => array(
'name' => 'Notes',
'add_new_item' => 'Add New Note',
'edit_item' => 'Edit Note',
'all_items' => 'All Notes',
'singular_name' => 'Note'
),
'menu_icon' => 'dashicons-welcome-write-blog'
));Capabilities yang Dibuat Otomatis:
| Capability | Fungsi |
|---|---|
edit_note | Mengedit satu note milik sendiri |
edit_notes | Mengedit note-note milik sendiri |
edit_others_notes | Mengedit note orang lain |
publish_notes | Mempublikasikan note |
delete_note | Menghapus satu note |
delete_notes | Menghapus note-note milik sendiri |
read_private_notes | Membaca note private orang lain |
Atur Permission via Members Plugin:
Untuk role Subscriber, berikan HANYA:
- ✅
edit_notes— bisa edit note sendiri - ✅
publish_notes— bisa buat note baru - ✅
delete_notes— bisa hapus note sendiri
JANGAN berikan:
- ❌
edit_others_notes— tidak boleh edit note orang lain - ❌
delete_others_notes— tidak boleh hapus note orang lain - ❌
read_private_notes— tidak boleh baca note orang lain
Penting: Jangan lupa berikan semua note capabilities ke Administrator juga, karena menambahkan
capability_typebaru akan menghapus akses default admin.
2. Status Private: Paksa di Server-Side
Masalah:
Meskipun kita kirim status: 'private' dari JavaScript, attacker bisa memanipulasi request dan mengirim status: 'publish' — note mereka akan terlihat oleh semua orang.
Solusi: wp_insert_post_data Filter
// Di functions.php
add_filter('wp_insert_post_data', 'makeNotePrivate', 10, 2);
function makeNotePrivate($data, $postArray) {
// Hanya berlaku untuk post type 'note'
if ($data['post_type'] == 'note') {
// Jangan paksa private saat di-trash (delete)
if ($data['post_status'] != 'trash') {
$data['post_status'] = 'private';
}
// Sanitize title dan content di server
$data['post_title'] = sanitize_text_field($data['post_title']);
$data['post_content'] = sanitize_textarea_field($data['post_content']);
}
return $data;
}Penjelasan Parameter:
| Parameter | Isi | Contoh |
|---|---|---|
$data | Data yang akan disimpan ke database | $data['post_status'], $data['post_title'], $data['post_content'] |
$postArray | Data post lengkap termasuk ID | $postArray['ID'] (0 jika post baru) |
Flow:
Client → REST API → wp_insert_post_data filter → Database
↑
PAKSA post_status = 'private'
+ sanitize title & contentCek post_status != 'trash':
Saat user menghapus note, WordPress set status ke 'trash'. Jika kita selalu paksa 'private', note tidak bisa dihapus! Maka hanya paksa 'private' jika status bukan 'trash'.
3. Sanitasi & Escape: Defense in Depth
Server-Side Sanitization (saat MENYIMPAN):
// Hapus tag HTML dan escape karakter berbahaya
sanitize_text_field($data['post_title']); // Untuk single-line text
sanitize_textarea_field($data['post_content']); // Untuk multi-line textFungsi sanitize_text_field():
- Menghapus semua tag HTML (
<script>,<b>, dll.) - Menghapus line breaks
- Strip octets (
%00) - Cocok untuk judul, nama, single-line input
Fungsi sanitize_textarea_field():
- Sama seperti
sanitize_text_field()tapi mempertahankan line breaks - Cocok untuk konten/body text
Client-Side Escaping (saat MENAMPILKAN):
| Fungsi | Konteks | Contoh |
|---|---|---|
esc_html() | Dalam tag HTML | <h2><?php echo esc_html($title); ?></h2> |
esc_attr() | Dalam atribut HTML | <input value="<?php echo esc_attr($title); ?>"> |
esc_textarea() | Dalam <textarea> | <textarea><?php echo esc_textarea($content); ?></textarea> |
esc_url() | Dalam URL | <a href="<?php echo esc_url($link); ?>"> |
Contoh Penerapan di Template:
<li data-id="<?php the_ID(); ?>">
<input readonly class="note-title-field" value="<?php echo esc_attr(get_the_title()); ?>">
<textarea readonly class="note-body-field"><?php echo esc_textarea(wp_strip_all_tags(get_the_content())); ?></textarea>
</li>4. Capability unfiltered_html
- Secara default, hanya Administrator dan Editor memiliki capability
unfiltered_html - Subscriber tidak memiliki capability ini
- Artinya: bahkan tanpa
sanitize_text_field(), WordPress sudah memblokir tag HTML dari Subscriber - TAPI, kita tetap menambahkan sanitasi sebagai defense in depth — lapisan keamanan ekstra
5. Hapus Prefix "Private:" dari Judul
Masalah:
Karena semua note berstatus private, WordPress otomatis menambahkan "Private: " di depan judul. Ini terlihat jelek di front-end.
Solusi:
// Di functions.php
add_filter('the_title', 'removePrivatePrefix');
function removePrivatePrefix($text) {
$text = str_replace('Private: ', '', $text);
return $text;
}6. Kurangi Subscriber Capabilities
Best Practice:
Berikan izin seminimal mungkin kepada setiap role.
Untuk Subscriber, pastikan hanya memiliki:
- ✅
read— akses dasar - ✅
edit_notes— edit note sendiri - ✅
publish_notes— buat note baru - ✅
delete_notes— hapus note sendiri
Lepaskan yang tidak diperlukan:
- ❌
read_private_notes— Subscriber hanya boleh baca note milik sendiri, bukan semua private notes
WordPress REST API sudah otomatis memfilter: user hanya bisa melihat, mengedit, dan menghapus private posts milik sendiri selama capability-nya benar.
Diagram Security Layers
Layer 1: CLIENT
└── Kirim status: 'private' dari JavaScript (mudah dibypass)
Layer 2: SERVER - wp_insert_post_data
└── PAKSA post_status = 'private' (tidak bisa dibypass client)
└── sanitize_text_field() + sanitize_textarea_field()
Layer 3: SERVER - WordPress Core
└── Cek unfiltered_html capability
└── Cek edit_notes / publish_notes capability
Layer 4: OUTPUT - Template
└── esc_html() / esc_attr() / esc_textarea()
└── wp_strip_all_tags()