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.
<?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>');
Isha
<b>Isha</b>
"onmouseover="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.
<form method="post">
<?php wp_nonce_field("myplugin_save", "myplugin_nonce"); ?>
<input type="text" name="title">
<button>Save</button>
</form>
<?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?
<?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()
<?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
<?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.
<?php
$raw = "O\\'Reilly"; // what $_POST can contain
echo sanitize_text_field(wp_unslash($raw));
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.
<?php
echo wp_validate_redirect("https://evil.example/phish", "/home") . "\n";
echo wp_validate_redirect("/account", "/home");
/home
/account
9. REST API permissions
Every custom REST route needs a permission_callback. For a public route, say so explicitly with "__return_true".
<?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);inwp-config.php. - Keep
WP_DEBUGoff (or logging only) on live sites. See Production Error Handling. - Make backups and test restoring them.
The short checklist for your own code
| Do | With |
|---|---|
| Sanitise input | sanitize_text_field(), absint(), sanitize_email() |
| Escape output | esc_html(), esc_attr(), esc_url(), wp_kses_post() |
| Verify intent | wp_verify_nonce() / check_admin_referer() |
| Verify permission | current_user_can() |
| Query safely | $wpdb->prepare() or the WP query classes |
| Guard files | if (!defined("ABSPATH")) exit; |