Skip to content

PHP Tutorial PHP for WordPress Developers

WordPress Security Basics

Lesson 336 of 369 3 min read By Deepak Paswan Updated Oct 5, 2026

The security rules from Chapter 26 apply in WordPress, with WordPress functions for each job. Most WordPress vulnerabilities come from plugins and themes that skip one of these.

1. Sanitise input, escape output

  • Sanitise when data comes in.
  • Escape when data goes out, for the place it goes to.
index.php
PHP
<?php
$input = '<script>alert(1)</script> Isha';

$clean = sanitize_text_field($input);

echo $clean . "\n";
echo esc_html('<b>' . $clean . '</b>') . "\n";
echo esc_attr('"onmouseover="x') . "\n";
echo wp_kses_post('<p>Hi <script>bad()</script><strong>there</strong></p>');
Output
OUTPUT
Isha
&lt;b&gt;Isha&lt;/b&gt;
&quot;onmouseover=&quot;x
<p>Hi bad()<strong>there</strong></p>

wp_kses_post() keeps the HTML that is safe in a post, and removes tags such as <script>.

2. Nonces: protect actions from CSRF

A nonce (“number used once”) in WordPress is a token tied to the user and the action. It is the WordPress version of the CSRF token.

index.php
PHP
<form method="post">
  <?php wp_nonce_field("myplugin_save", "myplugin_nonce"); ?>
  <input type="text" name="title">
  <button>Save</button>
</form>
index.php
PHP
<?php
if (!isset($_POST["myplugin_nonce"]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST["myplugin_nonce"])), "myplugin_save")) {
    wp_die("Security check failed.", "", ["response" => 403]);
}

For AJAX and REST requests, use check_ajax_referer() or let the REST API check the nonce header.

3. Capabilities: who may do this?

index.php
PHP
<?php
if (!current_user_can("manage_options")) {
    wp_die("You do not have permission.", "", ["response" => 403]);
}

Check a capability (edit_posts, manage_options), not a role name. Check it in every handler, even if the menu item is hidden from the user.

4. Database: always $wpdb->prepare()

index.php
PHP
<?php
global $wpdb;

// WRONG: SQL injection
$rows = $wpdb->get_results("SELECT * FROM {$wpdb->posts} WHERE post_title = '" . $_GET["t"] . "'");

// RIGHT
$rows = $wpdb->get_results(
    $wpdb->prepare("SELECT ID, post_title FROM {$wpdb->posts} WHERE post_title = %s", $_GET["t"])
);

Better still, use the higher-level functions (WP_Query, get_posts, update_post_meta) that prepare queries for you.

5. Block direct access

index.php
PHP
<?php
if (!defined("ABSPATH")) {
    exit;
}

Put this at the top of every PHP file in a plugin or theme except the main file that has the header.

6. Unslash and validate superglobals

WordPress adds slashes to $_GET, $_POST and $_COOKIE (a historical habit). Remove them with wp_unslash() before sanitising.

index.php
PHP
<?php
$raw = "O\\'Reilly";   // what $_POST can contain

echo sanitize_text_field(wp_unslash($raw));
Output
OUTPUT
O'Reilly

7. Uploads

Use wp_handle_upload() or the media functions, which check types against WordPress’s allowed list. Do not move files yourself with the visitor’s name (see Secure File Uploads).

8. Redirects

Use wp_safe_redirect() for redirects to URLs from input. It only allows your own domain.

index.php
PHP
<?php
echo wp_validate_redirect("https://evil.example/phish", "/home") . "\n";
echo wp_validate_redirect("/account", "/home");
Output
OUTPUT
/home
/account

9. REST API permissions

Every custom REST route needs a permission_callback. For a public route, say so explicitly with "__return_true".

index.php
PHP
<?php
register_rest_route("myplugin/v1", "/items", [
    "methods" => "POST",
    "callback" => "myplugin_create_item",
    "permission_callback" => fn() => current_user_can("edit_posts"),
]);

10. Site hygiene

  • Keep core, themes and plugins updated, and delete the ones you do not use.
  • Use strong, unique passwords and two-factor authentication for admins.
  • Limit login attempts, and use HTTPS.
  • Turn off file editing in the admin: define("DISALLOW_FILE_EDIT", true); in wp-config.php.
  • Keep WP_DEBUG off (or logging only) on live sites. See Production Error Handling.
  • Make backups and test restoring them.

The short checklist for your own code

DoWith
Sanitise inputsanitize_text_field(), absint(), sanitize_email()
Escape outputesc_html(), esc_attr(), esc_url(), wp_kses_post()
Verify intentwp_verify_nonce() / check_admin_referer()
Verify permissioncurrent_user_can()
Query safely$wpdb->prepare() or the WP query classes
Guard filesif (!defined("ABSPATH")) exit;
dangerous
Most plugin vulnerabilities are one missing check
A missing capability check, a missing nonce or an unescaped value. Build the habit of adding all four (sanitise, escape, nonce, capability) whenever you write a handler.