Skip to main content

Security, Validation, Sanitization, and Escaping

Security Boundary Model

BoundaryAction
Input enters PHPValidate and sanitize
User attempts actionCheck capability
Browser submits state changeVerify nonce
Data enters SQLUse prepared statements
Data leaves PHPEscape by output context
File is uploadedValidate permission, type, size, and name
Remote data arrivesValidate shape and sanitize fields

Validation vs Sanitization vs Escaping

StepQuestionExample
ValidateIs this acceptable?Allowed layout is grid or list
SanitizeHow do we clean it?sanitize_text_field()
EscapeHow do we print it safely?esc_html()

Untrusted Sources

  • $_GET
  • $_POST
  • $_REQUEST
  • $_COOKIE
  • uploaded files
  • REST parameters
  • AJAX payloads
  • remote API responses
  • imported CSV or JSON
  • saved options from older code
  • custom fields edited by users

WordPress Unslash

$raw = isset($_POST['title']) ? wp_unslash($_POST['title']) : '';
$title = sanitize_text_field($raw);

Sanitizers

DataFunction
Plain textsanitize_text_field()
Multiline textsanitize_textarea_field()
Keysanitize_key()
Slugsanitize_title()
Emailsanitize_email()
URL for storageesc_url_raw()
File namesanitize_file_name()
HTML classsanitize_html_class()
Integerabsint()

Escapers

Output ContextFunction
HTML textesc_html()
HTML attributeesc_attr()
URLesc_url()
JavaScript datawp_json_encode()
Textareaesc_textarea()
Allowed post HTMLwp_kses_post()

Capability Checks

if (! current_user_can('manage_options')) {
wp_die(esc_html__('Unauthorized.', 'my-plugin'));
}

Object-specific checks:

if (! current_user_can('edit_post', $post_id)) {
return;
}

Nonces

wp_nonce_field('myplugin_save', 'myplugin_nonce');
check_admin_referer('myplugin_save', 'myplugin_nonce');

Manual verification:

if (
! isset($_POST['myplugin_nonce']) ||
! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['myplugin_nonce'])), 'myplugin_save')
) {
wp_die(esc_html__('Invalid request.', 'my-plugin'));
}

Nonces are not permission checks. Use capabilities too.

SQL Safety

global $wpdb;

$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE ID = %d",
$user_id
)
);

Never concatenate untrusted values into SQL.

Redirect Safety

wp_safe_redirect(admin_url('options-general.php?page=my-plugin'));
exit;

REST Permission Callback

'permission_callback' => function (): bool {
return current_user_can('edit_posts');
},

Do not use __return_true for private data or write operations.

AJAX Security

function myplugin_ajax_save(): void {
check_ajax_referer('myplugin_ajax', 'nonce');

if (! current_user_can('edit_posts')) {
wp_send_json_error(['message' => __('Unauthorized.', 'my-plugin')], 403);
}

wp_send_json_success();
}

Common Vulnerabilities

VulnerabilityCausePrevention
XSSRaw outputEscape output
SQL injectionRaw SQL valuesPrepared statements
CSRFMissing nonceVerify nonce for state changes
Privilege escalationMissing capability checkCheck exact capability
File upload abuseWeak upload validationRestrict type, size, permission
Open redirectTrusting redirect URLUse wp_safe_redirect()
SSRFFetching arbitrary URLsAllow-list hosts or validate URLs

Production Checklist

  • Every request handler starts with capability and nonce/auth checks.
  • Every superglobal read uses wp_unslash() then sanitization.
  • Every SQL query with variable data uses $wpdb->prepare().
  • Every template output uses the correct escape function.
  • Every REST route has a real permission callback.
  • Every upload has permission, type, size, and destination checks.
  • Logs do not contain secrets.