WordPress Security Best Practices
OWASP Top 10 for WordPress
- 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'] ) ) );
- 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 );
- 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' );
- 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' ); }
- 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. }
- 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' ) ); }
- 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