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