Secure WordPress Plugin Development Best Practices
Learn essential security practices for WordPress plugin development. Build secure plugins that protect users from common vulnerabilities.
Developing secure WordPress plugins protects the millions of sites that may install your code. Understanding and implementing security best practices is essential for responsible plugin development.
Security Fundamentals
Core Principles
- Never trust user input
- Escape all output
- Use WordPress APIs
- Follow least privilege
- Fail securely
Input Validation
Sanitization Functions
// Always sanitize input data
$text = sanitize_text_field($_POST['title']);
$email = sanitize_email($_POST['email']);
$url = esc_url_raw($_POST['website']);
$html = wp_kses_post($_POST['content']);
$int = absint($_POST['quantity']);
$key = sanitize_key($_POST['option_name']);
$filename = sanitize_file_name($_POST['file']);
Validation Examples
// Validate before processing
function process_form_data($data) {
$errors = array();
// Required field
if (empty($data['name'])) {
$errors[] = 'Name is required';
}
// Email validation
if (!is_email($data['email'])) {
$errors[] = 'Invalid email address';
}
// Number range
$quantity = absint($data['quantity']);
if ($quantity < 1 || $quantity > 100) {
$errors[] = 'Quantity must be between 1 and 100';
}
return $errors;
}
Output Escaping
Context-Appropriate Escaping
// HTML context
echo '' . esc_html($user_input) . '
';
// HTML attributes
echo '';
// URLs
echo 'Link';
// JavaScript
echo '';
// SQL (use prepared statements instead)
$wpdb->prepare("SELECT * FROM table WHERE id = %d", $id);
Database Security
Prepared Statements
global $wpdb;
// CORRECT: Always use prepare()
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_table WHERE user_id = %d AND status = %s",
$user_id,
$status
)
);
// WRONG: Direct variable insertion
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}my_table WHERE user_id = $user_id"
);
Table Creation
// Use dbDelta for safe table creation
function create_plugin_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'my_plugin_data';
$sql = "CREATE TABLE $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
data text NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
Nonce Verification
Form Nonces
// In form
wp_nonce_field('my_plugin_action', 'my_plugin_nonce');
// Verification
function handle_form_submission() {
if (!isset($_POST['my_plugin_nonce']) ||
!wp_verify_nonce($_POST['my_plugin_nonce'], 'my_plugin_action')) {
wp_die('Security check failed');
}
// Process form...
}
AJAX Nonces
// Enqueue with nonce
wp_localize_script('my-script', 'MyPlugin', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce')
));
// AJAX handler
add_action('wp_ajax_my_action', function() {
check_ajax_referer('my_ajax_nonce', 'nonce');
// Process request...
wp_send_json_success($data);
});
Capability Checks
Permission Verification
// Check capabilities before actions
function admin_page_handler() {
if (!current_user_can('manage_options')) {
wp_die('Unauthorized access');
}
// Admin functionality...
}
// For post-specific actions
function edit_post_handler($post_id) {
if (!current_user_can('edit_post', $post_id)) {
wp_die('You cannot edit this post');
}
// Edit functionality...
}
File Security
Safe File Operations
// Validate file uploads
function handle_file_upload($file) {
$allowed_types = array('image/jpeg', 'image/png', 'image/gif');
// Check MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowed_types)) {
return new WP_Error('invalid_type', 'Invalid file type');
}
// Use WordPress upload handling
$upload = wp_handle_upload($file, array('test_form' => false));
if (isset($upload['error'])) {
return new WP_Error('upload_error', $upload['error']);
}
return $upload;
}
Options and Settings
Settings API
// Register settings with sanitization
register_setting(
'my_plugin_options',
'my_plugin_settings',
array(
'type' => 'array',
'sanitize_callback' => 'sanitize_my_settings'
)
);
function sanitize_my_settings($input) {
$sanitized = array();
$sanitized['api_key'] = sanitize_text_field($input['api_key'] ?? '');
$sanitized['enabled'] = !empty($input['enabled']);
$sanitized['limit'] = absint($input['limit'] ?? 10);
return $sanitized;
}
Security Review Checklist
- All user input sanitized
- All output escaped
- Prepared statements for SQL
- Nonces for forms and AJAX
- Capability checks implemented
- File uploads validated
- No direct file access
- Debug code removed
Conclusion
Secure plugin development requires consistent application of WordPress security APIs and best practices. Validate input, escape output, verify permissions, and use WordPress functions for all security-critical operations.
Written by Sarah Chen
WP Folder Shield Team