| name | wordpress-security-patterns |
| description | WordPress security best practices and vulnerability prevention patterns. Use when reviewing WordPress code for security issues, writing secure WordPress code, or checking for common vulnerabilities like SQL injection, XSS, CSRF, and authentication issues. |
WordPress Security Patterns
Comprehensive security patterns and best practices for WordPress development. This skill provides the knowledge base for identifying and preventing common WordPress security vulnerabilities.
SQL Injection Prevention
Core Principle
NEVER trust user input. ALWAYS use $wpdb->prepare() for database queries.
Required Pattern
global $wpdb;
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}posts WHERE post_author = %d AND post_status = %s",
$author_id,
$status
));
Format Specifiers
%d - Integer
%f - Float
%s - String
- Arrays: Use
implode(',', array_map('absint', $ids)) for IN clauses
Common Mistakes to Flag
$query = "SELECT * FROM wp_posts WHERE ID = {$_GET['id']}";
$wpdb->query($query);
$query = "SELECT * FROM wp_posts WHERE post_title LIKE '%" . $_GET['search'] . "%'";
$id = intval($_GET['id']);
$query = "SELECT * FROM wp_posts WHERE ID = $id";
Correct Patterns
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
absint($_GET['id'])
));
$results = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
sanitize_key($_POST['type']),
sanitize_key($_POST['status'])
));
$search = '%' . $wpdb->esc_like($_GET['s']) . '%';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$search
));
$ids = array_map('absint', $_POST['post_ids']);
$placeholders = (, (, (), ));
= ->(
,
...
);
Cross-Site Scripting (XSS) Prevention
Core Principle
NEVER output unescaped data. ALWAYS use appropriate escaping functions.
Escaping Functions Matrix
| Context | Function | Usage |
|---|
| HTML content | esc_html() | Plain text in HTML |
| HTML attributes | esc_attr() | Attributes like class, id, data-* |
| URLs | esc_url() | href, src attributes |
| JavaScript strings | esc_js() | Inline JavaScript strings |
| Textarea content | esc_textarea() | <textarea> content |
| Allowed HTML | wp_kses_post() | Content with safe HTML |
| Custom HTML | wp_kses() | With allowed tags array |
Required Patterns
Template Output:
<h1><?php echo esc_html($post_title); ?></h1>
<div class="<?php echo esc_attr($css_class); ?>"
data-id="<?php echo esc_attr($post_id); ?>">
// ✅ CORRECT - URLs
<a href="<?php echo esc_url($link); ?>">Link</a>
<img src="<?php echo esc_url($image_url); ?>" alt="">
// ✅ CORRECT - Mixed attributes
<input type="text"
name="<?php echo esc_attr($field_name); ?>"
value="<?php echo esc_attr($field_value); ?>"
placeholder="<?php echo esc_attr__('Enter ', ''); ?>">
JavaScript Context:
<script>
var message = '<?php echo esc_js($user_message); ?>';
</script>
wp_localize_script('my-script', 'myData', array(
'message' => $user_message, // Automatically escaped
'ajaxUrl' => admin_url('admin-ajax.php'),
));
Allowed HTML:
echo wp_kses_post($post_content);
$allowed_html = array(
'a' => array('href' => array(), 'title' => array()),
'br' => array(),
'em' => array(),
'strong' => array(),
);
echo wp_kses($custom_html, $allowed_html);
Common XSS Vulnerabilities to Flag
echo '<div>' . $_GET['message'] . '</div>';
echo '<div class="' . $_POST['class'] . '">';
echo '<a href="' . $_GET['redirect'] . '">Click</a>';
$clean = sanitize_text_field($_GET['input']);
echo '<div>' . $clean . '</div>';
echo '<script>var data = ' . json_encode($user_data) . ';</script>';
CSRF Protection (Nonces)
Core Principle
ALWAYS verify nonces for all state-changing operations.
Required Pattern - Forms
<form method="post" action="">
<?php wp_nonce_field('my_action_name', 'my_nonce_field'); ?>
<input type="text" name="field_name">
<?php submit_button(); ?>
</form>
if (!isset($_POST['my_nonce_field']) ||
!wp_verify_nonce($_POST['my_nonce_field'], 'my_action_name')) {
wp_die(__('Security check failed', 'textdomain'));
}
Required Pattern - AJAX
$.ajax({
url: ajaxData.ajaxUrl,
type: 'POST',
data: {
action: 'my_ajax_action',
nonce: ajaxData.nonce,
user_data: userData
}
});
add_action('wp_ajax_my_ajax_action', 'my_ajax_handler');
function my_ajax_handler() {
check_ajax_referer('my_ajax_nonce', 'nonce');
$data = sanitize_text_field($_POST['user_data']);
wp_send_json_success(array('result' => $data));
}
wp_enqueue_script('my-script', $url, array('jquery'), '1.0', true);
wp_localize_script('my-script', 'ajaxData', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'),
));
Required Pattern - URLs
$url = wp_nonce_url(
admin_url('admin-post.php?action=my_action'),
'my_action_nonce'
);
echo '<a href="' . esc_url($url) . '">Delete</a>';
if (!isset($_GET['_wpnonce']) ||
!wp_verify_nonce($_GET['_wpnonce'], 'my_action_nonce')) {
wp_die(__('Security check failed', 'textdomain'));
}
Nonce Vulnerabilities to Flag
<form method="post">
<input type="text" name="data">
<input type="submit">
</form>
if ($_POST['action'] === 'save') {
update_option('my_option', $_POST['value']);
}
add_action('wp_ajax_my_action', function() {
update_post_meta($_POST['post_id'], 'key', $_POST['value']);
});
Authentication & Authorization
Core Principle
ALWAYS check user capabilities before sensitive operations.
Required Patterns
Capability Checks:
if (!current_user_can('manage_options')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
if (!current_user_can('edit_post', $post_id)) {
wp_die(__('You cannot edit this post', 'textdomain'));
}
if (!current_user_can('edit_products')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
Common Capabilities:
manage_options - Admin settings
edit_posts - Edit posts
edit_pages - Edit pages
edit_users - Edit users
upload_files - Media uploads
edit_published_posts - Edit published content
- Custom capabilities for custom post types
Admin Page Protection:
add_menu_page(
'My Plugin',
'My Plugin',
'manage_options', // Required capability
'my-plugin',
'my_plugin_page'
);
function my_plugin_page() {
if (!current_user_can('manage_options')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
}
AJAX Handler Protection:
add_action('wp_ajax_save_settings', 'save_settings_handler');
function save_settings_handler() {
check_ajax_referer('save_settings_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array(
'message' => __('Insufficient permissions', 'textdomain')
));
}
}
Authorization Vulnerabilities to Flag
add_action('admin_post_delete_user', function() {
wp_delete_user($_POST['user_id']);
});
if ($_POST['user_role'] === 'administrator') {
do_admin_thing();
}
if (is_user_logged_in()) {
delete_post($_POST['post_id']);
}
Input Sanitization
Core Principle
Sanitize ALL input. Different data types need different sanitization.
Sanitization Functions
Text & Strings:
$text = sanitize_text_field($_POST['input']);
$textarea = sanitize_textarea_field($_POST['description']);
$email = sanitize_email($_POST['email']);
$url = sanitize_url($_POST['website']);
$filename = sanitize_file_name($_FILES['upload']['name']);
$class = sanitize_html_class($_POST['css_class']);
$key = sanitize_key($_POST['option_key']);
$title = sanitize_title($_POST['post_title']);
Numbers:
$id = absint($_POST['post_id']);
$value = intval($_POST['number']);
$price = floatval($_POST['price']);
Arrays:
$ids = array_map('absint', $_POST['post_ids']);
$fields = array_map('sanitize_text_field', $_POST['fields']);
function sanitize_array($array) {
foreach ($array as $key => &$value) {
if (is_array($value)) {
$value = sanitize_array($value);
} else {
$value = sanitize_text_field($value);
}
}
return $array;
}
Special Cases:
$content = wp_kses_post($_POST['content']);
$meta_key = sanitize_key($_POST['meta_key']);
$color = sanitize_hex_color($_POST['color']);
$username = sanitize_user($_POST['username']);
Sanitization Mistakes to Flag
update_option('my_option', $_POST['value']);
$id = sanitize_text_field($_POST['id']);
$email = sanitize_email($_POST['email']);
send_email($email);
$email = sanitize_email($_POST['email']);
if (!is_email($email)) {
return new WP_Error('invalid_email', __('Invalid email', 'textdomain'));
}
File Upload Security
Core Principle
NEVER trust uploaded files. Validate type, size, and use WordPress upload handlers.
Required Pattern
if (!function_exists('wp_handle_upload')) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
}
$uploadedfile = $_FILES['file'];
$upload_overrides = array(
'test_form' => false,
'mimes' => array(
'jpg|jpeg|jpe' => 'image/jpeg',
'png' => 'image/png',
'pdf' => 'application/pdf',
)
);
$movefile = wp_handle_upload($uploadedfile, $upload_overrides);
if ($movefile && !isset($movefile['error'])) {
$file_path = $movefile['file'];
$file_url = $movefile['url'];
} else {
$error = $movefile['error'];
}
File Upload Checks
if (!current_user_can('upload_files')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
check_ajax_referer('file_upload_nonce', 'nonce');
$allowed_types = array('image/jpeg', 'image/png', 'application/pdf');
$file_type = wp_check_filetype($_FILES['file']['name']);
if (!in_array($file_type['type'], $allowed_types)) {
wp_die(__('Invalid file type', 'textdomain'));
}
$max_size = 5 * 1024 * 1024;
if ($_FILES['file']['size'] > $max_size) {
wp_die(__('File too large', 'textdomain'));
}
File Upload Vulnerabilities to Flag
move_uploaded_file(
$_FILES['upload']['tmp_name'],
'/uploads/' . $_FILES['upload']['name']
);
copy($_FILES['upload']['tmp_name'], $destination);
if ($_FILES['upload']['type'] === 'image/jpeg') {
}
WordPress-Specific Security Patterns
Use WordPress Functions Over PHP
$response = wp_remote_get($url);
wp_safe_redirect($url);
wp_json_encode($data);
WP_Filesystem();
global $wp_filesystem;
$wp_filesystem->put_contents($file, $content);
Disable File Editing in Production
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);
API Endpoint Security
register_rest_route('myplugin/v1', '/data', array(
'methods' => 'POST',
'callback' => 'my_endpoint_callback',
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => array(
'title' => array(
'required' => true,
'validate_callback' => function($param) {
return is_string($param);
},
'sanitize_callback' => 'sanitize_text_field',
),
),
));
Priority Flags
When reviewing code, flag issues in this priority:
CRITICAL (Fix Immediately)
- SQL injection vulnerabilities
- Unescaped output (XSS)
- Missing nonce verification on state changes
- Missing capability checks on sensitive operations
- File upload without validation
HIGH (Fix Before Deploy)
- Using PHP functions instead of WordPress APIs
- Incorrect sanitization for data type
- Missing input validation
- Weak nonces (predictable or shared)
- Trusting client-side data
MEDIUM (Schedule Fix)
- Missing internationalization
- Deprecated WordPress functions
- Inefficient database queries
- Missing error handling
- Poor code organization
Testing Recommendations
When security issues are found, recommend:
- Manual testing with malicious input
- Using WordPress.com VIP code scanner
- Using PHPCS with WordPress security standards
- Penetration testing for critical applications
- Security audit by WordPress security specialist
References