wp-security
WordPress security best practices and vulnerability prevention for plugin development. Use when reviewing code for security issues or implementing security-sensitive features.
When & Why to Use This Skill
This Claude skill serves as a comprehensive security auditor and guide for WordPress plugin development. It focuses on preventing the OWASP Top 10 vulnerabilities within the WordPress ecosystem, providing developers with standardized code patterns for SQL injection prevention, XSS mitigation, CSRF protection, and secure data handling. By integrating WordPress-specific functions for sanitization, escaping, and authentication, it helps ensure that custom code meets the highest security standards.
Use Cases
- Performing automated security audits on WordPress plugin source code to identify missing sanitization or escaping functions.
- Implementing secure AJAX and REST API endpoints with proper nonce verification and capability checks to prevent unauthorized access.
- Hardening file upload systems by validating MIME types, file sizes, and preventing directory traversal attacks.
- Reviewing database queries to ensure the correct use of $wpdb->prepare() for SQL injection prevention.
- Configuring essential security headers and directory access restrictions to protect the WordPress environment from common web exploits.
| name | wp-security |
|---|---|
| description | WordPress security best practices and vulnerability prevention for plugin development. Use when reviewing code for security issues or implementing security-sensitive features. |
WordPress Security Best Practices
OWASP Top 10 for WordPress
1. SQL Injection Prevention
// WRONG - Never do this
$wpdb->query( "SELECT * FROM table WHERE id = " . $_GET['id'] );
// CORRECT - Always use prepare()
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}table WHERE id = %d",
absint( $_GET['id'] )
) );
2. Cross-Site Scripting (XSS) Prevention
// Escape all output
echo esc_html( $user_input );
echo '<a href="' . esc_url( $url ) . '">' . esc_html( $text ) . '</a>';
echo '<input value="' . esc_attr( $value ) . '" />';
// For allowed HTML
echo wp_kses_post( $content );
// For custom allowed tags
$allowed_html = array(
'a' => array(
'href' => array(),
'title' => array(),
),
);
echo wp_kses( $content, $allowed_html );
3. Cross-Site Request Forgery (CSRF) Prevention
// Form protection
<form method="post">
<?php wp_nonce_field( 'my_action', 'my_nonce' ); ?>
<!-- form fields -->
</form>
// Verification
if ( ! isset( $_POST['my_nonce'] ) ||
! wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
wp_die( __( 'Security check failed', 'text-domain' ) );
}
// AJAX nonce
wp_localize_script( 'my-script', 'myAjax', array(
'nonce' => wp_create_nonce( 'my-ajax-nonce' ),
) );
// AJAX verification
check_ajax_referer( 'my-ajax-nonce', 'nonce' );
4. Authentication and Authorization
// Always check capabilities
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Unauthorized access', 'text-domain' ) );
}
// Check user owns resource
if ( get_current_user_id() !== $post->post_author &&
! current_user_can( 'edit_others_posts' ) ) {
wp_die( __( 'Unauthorized', 'text-domain' ) );
}
// For AJAX
if ( ! is_user_logged_in() ) {
wp_send_json_error( 'Not logged in' );
}
5. Sensitive Data Exposure
// Never output sensitive data
// Use constants in wp-config.php
define( 'MY_API_KEY', 'secret_key' );
// Store securely
update_option( 'prefix_api_key', sanitize_text_field( $_POST['api_key'] ), 'no' );
// Never log sensitive data
if ( WP_DEBUG ) {
error_log( 'Error occurred' ); // Don't log passwords, API keys, etc.
}
6. File Upload Security
// Validate file type
$allowed_types = array( 'image/jpeg', 'image/png' );
$file_type = wp_check_filetype( $_FILES['file']['name'] );
if ( ! in_array( $file_type['type'], $allowed_types, true ) ) {
wp_die( __( 'Invalid file type', 'text-domain' ) );
}
// Use WordPress upload functions
$upload = wp_handle_upload( $_FILES['file'], array(
'test_form' => false,
'mimes' => array( 'jpg|jpeg|png' => 'image/jpeg' ),
) );
// Validate file size
if ( $_FILES['file']['size'] > 5000000 ) { // 5MB
wp_die( __( 'File too large', 'text-domain' ) );
}
7. Directory Traversal Prevention
// Validate paths
$file = basename( $_GET['file'] ); // Remove directory components
$file_path = plugin_dir_path( __FILE__ ) . 'uploads/' . $file;
// Verify file is within allowed directory
$real_path = realpath( $file_path );
$allowed_dir = realpath( plugin_dir_path( __FILE__ ) . 'uploads/' );
if ( strpos( $real_path, $allowed_dir ) !== 0 ) {
wp_die( __( 'Invalid file path', 'text-domain' ) );
}
Input Sanitization Cheat Sheet
// Text input
sanitize_text_field( $_POST['text'] );
// Textarea
sanitize_textarea_field( $_POST['textarea'] );
// Email
sanitize_email( $_POST['email'] );
// URL
esc_url_raw( $_POST['url'] ); // For database
esc_url( $url ); // For output
// Filename
sanitize_file_name( $_FILES['file']['name'] );
// HTML class
sanitize_html_class( $_POST['class'] );
// Key (alphanumeric, dashes, underscores)
sanitize_key( $_POST['key'] );
// Integer
absint( $_POST['id'] ); // Positive integer
intval( $_POST['number'] ); // Any integer
// HTML content
wp_kses_post( $_POST['content'] );
// Array of integers
array_map( 'absint', $_POST['ids'] );
Output Escaping Cheat Sheet
// HTML content
esc_html( $text );
esc_html__( 'Text', 'text-domain' ); // With translation
esc_html_e( 'Text', 'text-domain' ); // Echo with translation
// HTML attributes
esc_attr( $attribute );
// URLs
esc_url( $url );
// JavaScript
esc_js( $js_string );
// SQL (with $wpdb->prepare)
$wpdb->prepare( "SELECT * FROM table WHERE id = %d AND name = %s", $id, $name );
// Textarea
esc_textarea( $text );
// Allowed HTML
wp_kses_post( $content );
REST API Security
add_action( 'rest_api_init', 'prefix_register_secure_route' );
function prefix_register_secure_route() {
register_rest_route( 'plugin/v1', '/secure-endpoint', array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'prefix_secure_callback',
'permission_callback' => 'prefix_secure_permission_check',
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function( $param ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
),
),
) );
}
function prefix_secure_permission_check( $request ) {
// Verify nonce for logged-in users
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_forbidden',
__( 'You do not have permission', 'text-domain' ),
array( 'status' => 403 )
);
}
return true;
}
File Access Protection
// Add to index.php in plugin directories
<?php
// Silence is golden.
// Or prevent direct access in all PHP files
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
Secure AJAX Handlers
// Register AJAX handler
add_action( 'wp_ajax_my_action', 'prefix_ajax_handler' );
add_action( 'wp_ajax_nopriv_my_action', 'prefix_ajax_handler' ); // For logged-out users
function prefix_ajax_handler() {
// Verify nonce
check_ajax_referer( 'my-ajax-nonce', 'nonce' );
// Check capabilities
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Unauthorized' );
}
// Sanitize input
$data = sanitize_text_field( $_POST['data'] );
// Process and return
wp_send_json_success( array( 'result' => $data ) );
}
Security Headers
// Add security headers
add_action( 'send_headers', 'prefix_add_security_headers' );
function prefix_add_security_headers() {
header( 'X-Content-Type-Options: nosniff' );
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'X-XSS-Protection: 1; mode=block' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
}
Common Vulnerability Patterns to Avoid
- Trusting user input - Always sanitize and validate
- Direct file includes - Validate file paths
- Unprotected AJAX - Always verify nonce and capabilities
- SQL concatenation - Always use
$wpdb->prepare() - Missing output escaping - Escape everything
- Weak nonce checks - Always verify before processing
- Missing capability checks - Check permissions
- Exposed error messages - Don't reveal system information
- Unvalidated redirects - Use
wp_safe_redirect() - Hardcoded secrets - Use constants and environment variables