Security: Nonces, Sanitization & Capabilities
Security Fundamentals
Plugin security follows four principles:
- Authentication – Is the user logged in?
- Authorization – Can this user perform this action?
- Validation – Is the input data valid?
- Sanitization/Escaping – Is data safe for storage/display?
Nonces: Preventing CSRF Attacks
Nonces verify that requests come from legitimate forms:
// Creating a nonce
$nonce = wp_create_nonce( 'tm_delete_task' );
// In a URL
$delete_url = add_query_arg( [
'action' => 'delete',
'task_id' => $task_id,
'_wpnonce' => wp_create_nonce( "tm_delete_{$task_id}" ),
], admin_url( 'admin.php?page=task-manager' ) );
// In a form
<form method="post">
<?php wp_nonce_field( 'tm_save_task', 'tm_nonce' ); ?>
<!-- form fields -->
</form>Verifying Nonces
// Verify form nonce
if ( ! wp_verify_nonce( $_POST['tm_nonce'] ?? '', 'tm_save_task' ) ) {
wp_die( __( 'Security check failed.', 'task-manager' ) );
}
// Verify URL nonce (for admin actions)
if ( ! wp_verify_nonce( $_GET['_wpnonce'] ?? '', "tm_delete_{$task_id}" ) ) {
wp_die( __( 'Invalid security token.', 'task-manager' ) );
}
// For AJAX - exits on failure
check_ajax_referer( 'tm_admin_nonce', 'nonce' );
Nonce Lifetime: Nonces are valid for 24 hours by default. For sensitive operations, you may want to add timestamp checks.
User Capabilities
Never trust user roles directly – check capabilities:
// ❌ Wrong - checking role
if ( in_array( 'administrator', wp_get_current_user()->roles ) ) {
// do something
}
// ✅ Correct - checking capability
if ( current_user_can( 'manage_options' ) ) {
// do something
}Common Capabilities
| Capability | Who Has It | Use For |
|---|---|---|
read | All logged-in users | View content |
edit_posts | Authors+ | Create/edit content |
publish_posts | Authors+ | Publish content |
delete_posts | Authors+ | Delete own content |
manage_options | Administrators | Plugin settings |
install_plugins | Super Admins | Install plugins |
Creating Custom Capabilities
// On activation
public static function activate(): void {
// Add custom capability to admin role
$admin_role = get_role( 'administrator' );
$admin_role->add_cap( 'manage_tasks' );
// Create custom role
add_role( 'task_manager', __( 'Task Manager', 'task-manager' ), [
'read' => true,
'manage_tasks' => true,
] );
}
// On uninstall
public static function uninstall(): void {
remove_role( 'task_manager' );
$admin_role = get_role( 'administrator' );
$admin_role->remove_cap( 'manage_tasks' );
}
// Usage
if ( current_user_can( 'manage_tasks' ) ) {
// User can manage tasks
}Data Sanitization
Sanitize all input before storage or use:
String Sanitization
// Basic text - removes tags, encoded entities
$title = sanitize_text_field( $_POST['title'] );
// Textarea - preserves line breaks
$description = sanitize_textarea_field( $_POST['description'] );
// Email
$email = sanitize_email( $_POST['email'] );
// URL
$url = esc_url_raw( $_POST['url'] );
// Filename
$filename = sanitize_file_name( $_POST['filename'] );
// HTML class
$class = sanitize_html_class( $_POST['class'] );
// Key (lowercase, alphanumeric, dashes, underscores)
$status = sanitize_key( $_POST['status'] );Number Sanitization
// Absolute integer (positive)
$id = absint( $_POST['id'] );
// Integer (can be negative)
$count = intval( $_POST['count'] );
// Float
$price = floatval( $_POST['price'] );Array Sanitization
// Sanitize array of integers
$ids = array_map( 'absint', (array) $_POST['task_ids'] );
// Sanitize array of strings
$tags = array_map( 'sanitize_text_field', (array) $_POST['tags'] );
// Filter valid values only
$allowed = [ 'pending', 'in_progress', 'completed' ];
$statuses = array_intersect( (array) $_POST['statuses'], $allowed );KSES: Allow Specific HTML
// Allow basic HTML
$content = wp_kses_post( $_POST['content'] );
// Allow custom tags/attributes
$allowed_html = [
'a' => [
'href' => [],
'title' => [],
'target' => [],
],
'strong' => [],
'em' => [],
'code' => [],
];
$content = wp_kses( $_POST['content'], $allowed_html );Output Escaping
Always escape when outputting:
// Escape HTML entities (most common)
echo esc_html( $task->title );
// Escape attributes
<input value="<?php echo esc_attr( $value ); ?>">
// Escape URLs
<a href="<?php echo esc_url( $url ); ?>">
// Escape JavaScript strings
<script>var data = <?php echo esc_js( $data ); ?>;</script>
// Escape for JSON in attributes
<div data-config='<?php echo esc_attr( wp_json_encode( $config ) ); ?>'>
// Escape with translation
echo esc_html__( 'Hello World', 'task-manager' );
echo esc_html_e( 'Hello World', 'task-manager' ); // echoes directly
// Escape with placeholders
printf(
esc_html__( 'Task %s created by %s', 'task-manager' ),
esc_html( $task->title ),
esc_html( $user->display_name )
);Complete Security Example
<?php
// Secure form processing
public function handle_form_submission(): void {
// 1. Check if form was submitted
if ( ! isset( $_POST['tm_submit'] ) ) {
return;
}
// 2. Verify nonce (CSRF protection)
if ( ! wp_verify_nonce( $_POST['tm_nonce'] ?? '', 'tm_create_task' ) ) {
wp_die(
__( 'Security verification failed.', 'task-manager' ),
__( 'Error', 'task-manager' ),
[ 'response' => 403, 'back_link' => true ]
);
}
// 3. Check user capabilities (authorization)
if ( ! current_user_can( 'manage_tasks' ) ) {
wp_die(
__( 'You do not have permission to create tasks.', 'task-manager' ),
__( 'Permission Denied', 'task-manager' ),
[ 'response' => 403, 'back_link' => true ]
);
}
// 4. Validate required fields
$title = isset( $_POST['title'] ) ? trim( $_POST['title'] ) : '';
if ( empty( $title ) ) {
$this->add_error( 'title', __( 'Title is required.', 'task-manager' ) );
return;
}
// 5. Validate field formats
$status = $_POST['status'] ?? 'pending';
if ( ! in_array( $status, [ 'pending', 'in_progress', 'completed' ], true ) ) {
$status = 'pending';
}
$priority = absint( $_POST['priority'] ?? 1 );
$priority = max( 1, min( 5, $priority ) ); // Clamp to 1-5
// 6. Sanitize data
$data = [
'title' => sanitize_text_field( $title ),
'description' => wp_kses_post( $_POST['description'] ?? '' ),
'status' => sanitize_key( $status ),
'priority' => $priority,
'due_date' => $this->validate_date( $_POST['due_date'] ?? '' ),
];
// 7. Create record
$task_id = $this->db->create( $data );
if ( ! $task_id ) {
$this->add_error( 'general', __( 'Failed to create task.', 'task-manager' ) );
return;
}
// 8. Redirect with success message
wp_safe_redirect(
add_query_arg( 'message', 'created', admin_url( 'admin.php?page=task-manager' ) )
);
exit;
}
private function validate_date( string $date ): ?string {
if ( empty( $date ) ) {
return null;
}
$timestamp = strtotime( $date );
if ( ! $timestamp ) {
return null;
}
return date( 'Y-m-d H:i:s', $timestamp );
}Security Checklist
| Check | Method |
|---|---|
| CSRF Protection | wp_nonce_field(), check_admin_referer() |
| Authorization | current_user_can() |
| Input Sanitization | sanitize_() functions |
| Output Escaping | esc_() functions |
| SQL Injection | $wpdb->prepare() |
| Direct File Access | if ( ! defined( 'ABSPATH' ) ) exit; |
| File Uploads | wp_check_filetype(), size limits |
Next Steps
In our final lesson, we'll package the plugin for distribution, handle translations, and prepare for WordPress.org submission.
🎯 Lesson Complete! Your plugin is now secure against common vulnerabilities.