Security Best Practices

Because WordPress powers such a huge share of the web, it's a constant target for attackers. Most real-world WordPress vulnerabilities aren't exotic — they come from a handful of well understood mistakes: trusting user input, forgetting to check permissions, or building SQL queries by hand. This page covers the core defensive habits every WordPress developer needs.

1. Nonces: CSRF Protection

A nonce ("number used once") is a short-lived token WordPress generates to prove that a form submission or AJAX request genuinely came from a page it rendered — not from a malicious site tricking a logged-in user's browser into submitting a hidden form (a Cross-Site Request Forgery, or CSRF, attack). A nonce is created with wp_create_nonce() and checked with wp_verify_nonce() or check_admin_referer().

// In the form template
<form method="post">
    <?php wp_nonce_field('save_profile_action', 'profile_nonce'); ?>
    <input type="text" name="display_name">
    <input type="submit" value="Save">
</form>

<?php
// In the handler that processes the submission
function handle_profile_save() {
    if (
        !isset($_POST['profile_nonce']) ||
        !wp_verify_nonce($_POST['profile_nonce'], 'save_profile_action')
    ) {
        wp_die('Security check failed.');
    }
    // ...safe to proceed
}
Tip: The string passed to both wp_create_nonce()/wp_nonce_field() and wp_verify_nonce() must match exactly — it acts like a namespace, so a nonce created for one action can't be replayed against a different one.

2. Sanitize Input vs. Escape Output

This distinction trips up a lot of WordPress developers, but the rule is simple once it clicks: sanitize on the way in, escape on the way out. Sanitizing cleans data before it's saved (e.g. to the database); escaping makes data safe right at the moment it's printed into HTML, to prevent Cross-Site Scripting (XSS).

FunctionWhenPurpose
sanitize_text_field()Input (in)Strips tags, extra whitespace, and line breaks from a plain text field before it's stored
esc_html()Output (out)Escapes a string so it prints as visible text, not interpreted HTML — use when echoing text between tags
esc_attr()Output (out)Escapes a string for safe use inside an HTML attribute, e.g. value="<?php echo esc_attr($x); ?>"
esc_url()Output (out)Escapes and validates a URL before it's placed in href or src
Note: Sanitizing does not make data safe to print as HTML later, and escaping does not make data safe to store as-is — they solve two different problems and both steps are usually needed.

3. Always Use $wpdb->prepare() for Custom Queries

Any time a custom SQL query includes a variable — a post ID, a search term, anything from user input — that value must go through $wpdb->prepare(), which uses placeholders (%s, %d, %f) to safely escape values, exactly like the prepared statements covered in The WordPress Database.

<?php
global $wpdb;

// Never do this — vulnerable to SQL injection:
// $wpdb->get_results("SELECT * FROM wp_posts WHERE post_author = $author_id");

// Always do this instead:
$results = $wpdb->get_results(
    $wpdb->prepare("SELECT * FROM {$wpdb->posts} WHERE post_author = %d", $author_id)
);

4. Capability Checks Before Sensitive Actions

Before allowing any sensitive action — showing an admin settings page, deleting data, changing a setting — always verify the current user is actually allowed to do it, with current_user_can().

<?php
function render_plugin_settings_page() {
    if (!current_user_can('manage_options')) {
        wp_die('You do not have permission to access this page.');
    }
    // ...render the settings form
}
Tip: manage_options is the capability administrators hold; other roles (editor, author, subscriber) hold different, more limited capabilities — always check the specific capability the action actually requires, not just whether someone is "logged in."

5. Keep Core, Themes, and Plugins Updated

A huge share of real-world WordPress hacks exploit known, already-patched vulnerabilities in outdated software — not zero-days. Updating WordPress core, the active theme, and every plugin promptly is one of the simplest and most effective security practices available.

6. Putting It All Together

A realistic form handler combines every technique above: verify the nonce, sanitize the input, check the capability, and only then save the data.

<?php
function handle_site_tagline_update() {
    // 1. Verify the nonce (CSRF protection)
    if (
        !isset($_POST['tagline_nonce']) ||
        !wp_verify_nonce($_POST['tagline_nonce'], 'update_tagline')
    ) {
        wp_die('Security check failed.');
    }

    // 2. Check the capability
    if (!current_user_can('manage_options')) {
        wp_die('You do not have permission to do this.');
    }

    // 3. Sanitize the raw input — never trust $_POST directly
    $tagline = sanitize_text_field($_POST['tagline'] ?? '');

    // 4. Now it's safe to save
    update_option('blogdescription', $tagline);

    wp_redirect(admin_url('options-general.php?updated=true'));
    exit;
}
Note: Every one of these four steps is independent — skipping any single one (say, checking the capability but forgetting the nonce) still leaves a real vulnerability. Always apply all four together.
Next up: With security fundamentals covered, Best Practices wraps up the whole track with a summary checklist for professional WordPress development.