| 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
// ALWAYS use this 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
// ❌ VULNERABLE - Direct variable insertion
$query = "SELECT * FROM wp_posts WHERE ID = {$_GET['id']}";
$wpdb->query($query);
// ❌ VULNERABLE - String concatenation
$query = "SELECT * FROM wp_posts WHERE post_title LIKE '%" . $_GET['search'] . "%'";
// ❌ VULNERABLE - Even with sanitization, use prepare()
$id = intval($_GET['id']);
$query = "SELECT * FROM wp_posts WHERE ID = $id"; // Still wrong!
Correct Patterns
// ✅ CORRECT - Prepared statement
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
absint($_GET['id'])
));
// ✅ CORRECT - Multiple parameters
$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'])
));
// ✅ CORRECT - LIKE queries
$search = '%' . $wpdb->esc_like($_GET['s']) . '%';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$search
));
// ✅ CORRECT - IN clause with integers
$ids = array_map('absint', $_POST['post_ids']);
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
...$ids
);
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:
// ✅ CORRECT - HTML content
<h1><?php echo esc_html($post_title); ?></h1>
// ✅ CORRECT - HTML attributes
<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 text', 'textdomain'); ?>">
JavaScript Context:
// ✅ CORRECT - JavaScript string
<script>
var message = '<?php echo esc_js($user_message); ?>';
</script>
// ✅ CORRECT - Better: Use wp_localize_script()
wp_localize_script('my-script', 'myData', array(
'message' => $user_message, // Automatically escaped
'ajaxUrl' => admin_url('admin-ajax.php'),
));
Allowed HTML:
// ✅ CORRECT - Post content with safe HTML
echo wp_kses_post($post_content);
// ✅ CORRECT - Custom allowed tags
$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
// ❌ VULNERABLE - No escaping
echo '<div>' . $_GET['message'] . '</div>';
// ❌ VULNERABLE - Unescaped attribute
echo '<div class="' . $_POST['class'] . '">';
// ❌ VULNERABLE - Unescaped URL
echo '<a href="' . $_GET['redirect'] . '">Click</a>';
// ❌ VULNERABLE - Even with sanitization
$clean = sanitize_text_field($_GET['input']);
echo '<div>' . $clean . '</div>'; // Still needs esc_html()!
// ❌ VULNERABLE - JSON in HTML
echo '<script>var data = ' . json_encode($user_data) . ';</script>';
// Should use wp_json_encode() which escapes properly
CSRF Protection (Nonces)
Core Principle
ALWAYS verify nonces for all state-changing operations.
Required Pattern - Forms
// ✅ CORRECT - Form with nonce
<form method="post" action="">
<?php wp_nonce_field('my_action_name', 'my_nonce_field'); ?>
<input type="text" name="field_name">
<?php submit_button(); ?>
</form>
// ✅ CORRECT - Verification
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
// ✅ CORRECT - JavaScript (after wp_localize_script)
$.ajax({
url: ajaxData.ajaxUrl,
type: 'POST',
data: {
action: 'my_ajax_action',
nonce: ajaxData.nonce,
user_data: userData
}
});
// ✅ CORRECT - PHP handler
add_action('wp_ajax_my_ajax_action', 'my_ajax_handler');
function my_ajax_handler() {
check_ajax_referer('my_ajax_nonce', 'nonce');
// Process request
$data = sanitize_text_field($_POST['user_data']);
wp_send_json_success(array('result' => $data));
}
// ✅ CORRECT - Enqueue with localized nonce
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
// ✅ CORRECT - URL with nonce
$url = wp_nonce_url(
admin_url('admin-post.php?action=my_action'),
'my_action_nonce'
);
echo '<a href="' . esc_url($url) . '">Delete</a>';
// ✅ CORRECT - Verification
if (!isset($_GET['_wpnonce']) ||
!wp_verify_nonce($_GET['_wpnonce'], 'my_action_nonce')) {
wp_die(__('Security check failed', 'textdomain'));
}
Nonce Vulnerabilities to Flag
// ❌ VULNERABLE - No nonce in form
<form method="post">
<input type="text" name="data">
<input type="submit">
</form>
// ❌ VULNERABLE - No nonce verification
if ($_POST['action'] === 'save') {
update_option('my_option', $_POST['value']); // Unprotected!
}
// ❌ VULNERABLE - AJAX without nonce
add_action('wp_ajax_my_action', function() {
// No check_ajax_referer()!
update_post_meta($_POST['post_id'], 'key', $_POST['value']);
});
Authentication & Authorization
Core Principle
ALWAYS check user capabilities before sensitive operations.
Required Patterns
Capability Checks:
// ✅ CORRECT - Basic capability check
if (!current_user_can('manage_options')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
// ✅ CORRECT - Post-specific capability
if (!current_user_can('edit_post', $post_id)) {
wp_die(__('You cannot edit this post', 'textdomain'));
}
// ✅ CORRECT - Custom post type capability
if (!current_user_can('edit_products')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
Common Capabilities:
manage_options- Admin settingsedit_posts- Edit postsedit_pages- Edit pagesedit_users- Edit usersupload_files- Media uploadsedit_published_posts- Edit published content- Custom capabilities for custom post types
Admin Page Protection:
// ✅ CORRECT - Admin page with capability
add_menu_page(
'My Plugin',
'My Plugin',
'manage_options', // Required capability
'my-plugin',
'my_plugin_page'
);
function my_plugin_page() {
// Double-check capability
if (!current_user_can('manage_options')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
// Page content
}
AJAX Handler Protection:
// ✅ CORRECT - Protected AJAX handler
add_action('wp_ajax_save_settings', 'save_settings_handler');
function save_settings_handler() {
// Check nonce
check_ajax_referer('save_settings_nonce', 'nonce');
// Check capability
if (!current_user_can('manage_options')) {
wp_send_json_error(array(
'message' => __('Insufficient permissions', 'textdomain')
));
}
// Process request
}
Authorization Vulnerabilities to Flag
// ❌ VULNERABLE - No capability check
add_action('admin_post_delete_user', function() {
wp_delete_user($_POST['user_id']); // Anyone can delete!
});
// ❌ VULNERABLE - Trusting user roles
if ($_POST['user_role'] === 'administrator') { // Can be spoofed!
do_admin_thing();
}
// ❌ VULNERABLE - Checking logged-in status only
if (is_user_logged_in()) {
delete_post($_POST['post_id']); // Any logged-in user!
}
Input Sanitization
Core Principle
Sanitize ALL input. Different data types need different sanitization.
Sanitization Functions
Text & Strings:
// ✅ Simple text (strips tags and newlines)
$text = sanitize_text_field($_POST['input']);
// ✅ Textarea (strips tags, preserves newlines)
$textarea = sanitize_textarea_field($_POST['description']);
// ✅ Email
$email = sanitize_email($_POST['email']);
// ✅ URL
$url = sanitize_url($_POST['website']);
// ✅ Filename
$filename = sanitize_file_name($_FILES['upload']['name']);
// ✅ HTML class
$class = sanitize_html_class($_POST['css_class']);
// ✅ Key (lowercase alphanumeric + underscores)
$key = sanitize_key($_POST['option_key']);
// ✅ Title (for use in title tags)
$title = sanitize_title($_POST['post_title']);
Numbers:
// ✅ Integer (positive only)
$id = absint($_POST['post_id']);
// ✅ Integer (positive or negative)
$value = intval($_POST['number']);
// ✅ Float
$price = floatval($_POST['price']);
Arrays:
// ✅ Array of integers
$ids = array_map('absint', $_POST['post_ids']);
// ✅ Array of text fields
$fields = array_map('sanitize_text_field', $_POST['fields']);
// ✅ Recursive sanitization
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:
// ✅ Rich content (with allowed HTML)
$content = wp_kses_post($_POST['content']);
// ✅ Meta key
$meta_key = sanitize_key($_POST['meta_key']);
// ✅ Hex color
$color = sanitize_hex_color($_POST['color']);
// ✅ Username (for user_login)
$username = sanitize_user($_POST['username']);
Sanitization Mistakes to Flag
// ❌ No sanitization
update_option('my_option', $_POST['value']);
// ❌ Wrong function for data type
$id = sanitize_text_field($_POST['id']); // Should be absint()
// ❌ Sanitizing but not validating
$email = sanitize_email($_POST['email']);
send_email($email); // What if it's not a valid email format?
// ✅ CORRECT - Sanitize AND validate
$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
// ✅ CORRECT - Using WordPress upload handler
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 uploaded successfully
$file_path = $movefile['file'];
$file_url = $movefile['url'];
} else {
// Error handling
$error = $movefile['error'];
}
File Upload Checks
// ✅ Check capabilities
if (!current_user_can('upload_files')) {
wp_die(__('Insufficient permissions', 'textdomain'));
}
// ✅ Check nonce
check_ajax_referer('file_upload_nonce', 'nonce');
// ✅ Validate file type
$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'));
}
// ✅ Check file size (5MB example)
$max_size = 5 * 1024 * 1024; // 5MB
if ($_FILES['file']['size'] > $max_size) {
wp_die(__('File too large', 'textdomain'));
}
File Upload Vulnerabilities to Flag
// ❌ VULNERABLE - Direct file move
move_uploaded_file(
$_FILES['upload']['tmp_name'],
'/uploads/' . $_FILES['upload']['name']
);
// ❌ VULNERABLE - No type validation
copy($_FILES['upload']['tmp_name'], $destination);
// ❌ VULNERABLE - Trusting client-provided MIME type
if ($_FILES['upload']['type'] === 'image/jpeg') { // Can be spoofed!
// ...
}
WordPress-Specific Security Patterns
Use WordPress Functions Over PHP
// ✅ Use WordPress HTTP API
$response = wp_remote_get($url);
// ❌ Don't use: file_get_contents($url)
// ✅ Use WordPress redirect
wp_safe_redirect($url);
// ❌ Don't use: header('Location: ' . $url);
// ✅ Use WordPress JSON encoding
wp_json_encode($data);
// ❌ Don't use: json_encode($data);
// ✅ Use WordPress filesystem API
WP_Filesystem();
global $wp_filesystem;
$wp_filesystem->put_contents($file, $content);
// ❌ Don't use: file_put_contents($file, $content);
Disable File Editing in Production
// ✅ Add to wp-config.php
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);
API Endpoint Security
// ✅ Register secured REST API endpoint
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