Most WordPress security problems do not come from clever attackers breaking WordPress itself. They come from small gaps in themes and plugins: a form that trusts whatever is submitted, a value printed straight onto the page, a settings screen any logged-in user can reach. Each gap is usually one missing function call. The frightening part is how ordinary the code looks; the reassuring part is how few functions you need to close most of them.
This guide covers the handful of WordPress functions that prevent the majority of vulnerabilities, grouped into four simple habits: sanitize what comes in, escape what goes out, verify intent with nonces, and check permissions. Each comes with a real example.
The mental model: trust nothing that crosses a boundary
Data changes hands at boundaries: when a visitor submits a form, when a URL parameter is read, when a value from the database is printed into HTML, when an AJAX request arrives. At each boundary, ask two questions:
- Coming in: is this data what I expect, and is it safe to store?
- Going out: could this data break the HTML, JavaScript or URL it is placed into?
WordPress provides a function for each situation. Using them consistently is most of the job.
Habit 1: Sanitize input
Sanitizing cleans data as it arrives, before you store or use it. Pick the function that matches what the value should be:
| Function | Use for |
|---|---|
sanitize_text_field() | Single-line text, such as names and titles |
sanitize_textarea_field() | Multi-line plain text |
sanitize_email() | Email addresses |
absint() | IDs and other non-negative whole numbers |
sanitize_key() | Lowercase keys and slugs |
esc_url_raw() | URLs you are about to save |
wp_kses_post() | HTML that should allow only safe post formatting |
Values from $_POST and $_GET may be slashed, so unslash them first:
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
Sanitizing is not the same as validating. After cleaning, check the value makes sense: is the email address valid according to is_email(), is the ID a post that exists, is the option one of the allowed choices?
Habit 2: Escape output
Escaping happens at the moment you print data, and it depends on where the data is going. This is the defence against cross-site scripting (XSS), where malicious script ends up running in someone else's browser.
| Function | Use when printing into |
|---|---|
esc_html() | Normal HTML text |
esc_attr() | An HTML attribute value |
esc_url() | An href or src |
esc_js() | Inline JavaScript strings |
wp_kses_post() | Content that may contain safe HTML formatting |
<a href="" title="">
The golden rule is escape late: escape right where you echo, even if you believe the value was sanitized earlier. Code changes, data sources change, and late escaping keeps each output safe on its own.
Habit 3: Verify intent with nonces
A nonce is a token that proves a request came from your own form, protecting against cross-site request forgery (CSRF), where another website tricks a logged-in user's browser into submitting a request. Add one to every form that changes something:
<button type="submit">Save</button>
And check it before processing:
if ( ! isset( $_POST['acme_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['acme_nonce'] ) ), 'acme_save_settings' ) ) {
wp_die( esc_html__( 'Security check failed.', 'acme' ) );
}
For AJAX, create the nonce with wp_create_nonce(), send it with the request, and check it with check_ajax_referer().
Habit 4: Check capabilities
A nonce proves the request came from your form. It does not prove the person is allowed to do the action. Always pair it with a capability check:
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to do this.', 'acme' ) );
}
Choose the capability that matches the action: manage_options for site settings, edit_posts for content editing, edit_post with an ID for a specific post. For REST API routes, put the check in the permission_callback, and never use __return_true for anything that changes data.
Putting it together: a safe settings handler
function acme_handle_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Not allowed.', 'acme' ) );
}
check_admin_referer( 'acme_save_settings', 'acme_nonce' );
$title = isset( $_POST['acme_title'] )
? sanitize_text_field( wp_unslash( $_POST['acme_title'] ) )
: '';
update_option( 'acme_title', $title );
wp_safe_redirect( admin_url( 'options-general.php?page=acme&updated=1' ) );
exit;
}
add_action( 'admin_post_acme_save', 'acme_handle_settings' );
Every habit appears here: permission check, nonce check, sanitized input, and a safe redirect followed by exit, which is easy to forget and important, because a redirect only sends a header and the code would otherwise keep running.
Database queries
Prefer WordPress's own functions, like get_posts(), WP_Query and update_option(), which handle safety for you. When you genuinely need a custom SQL query, always use $wpdb->prepare() with placeholders instead of putting variables directly into the query. That single habit prevents SQL injection.
Mistakes reviewers catch again and again
- Escaping on input instead of output. Storing
esc_html()-encoded text in the database leads to double-encoded characters later. Sanitize on the way in, escape on the way out. - Using the wrong escaping function, such as
esc_html()inside anhref. A URL needsesc_url(), an attribute needsesc_attr(). - Trusting data from your own database. If a user could have written it, treat it as untrusted when you print it.
- Checking a nonce but not a capability, or the other way round. You almost always need both.
- Forgetting
exitafter a redirect, so the rest of the handler still runs.
Quick reference
The WordPress Functions Reference covers the core security functions from this guide, including sanitize_text_field(), absint(), esc_html(), esc_attr(), esc_url(), wp_kses_post(), the nonce functions and current_user_can(), alongside the most-used functions for queries, hooks, assets, registration and caching, each with a short working example and a link to its official documentation. Press / to jump to the search box and type a task, such as "nonce" or "escape".
Building it in from the start
Security is much easier to build in than to bolt on. When you start a new plugin, the WordPress Plugin Generator produces a structure with prefixed names, escaped output and a settings page built on the Settings API, which handles nonces for you. Our guide to creating your first WordPress plugin walks through it step by step. Before launch, the WordPress Security Checker is a useful final review of your site's configuration.
If you work with AI assistants while coding, avoid pasting real credentials, keys or customer data into the chat; our guide to using ChatGPT without sharing personal data explains a safe workflow.
Four habits, most problems solved
Sanitize what comes in. Escape what goes out, as late as possible. Verify intent with nonces. Check permissions every time. None of these functions is complicated, and once they become habits, you will find yourself spotting the gaps in other people's code too, which is exactly how good WordPress developers think.




