Skip to the content.

WordPress Security

WordPress powers over 43% of the internet and has a huge surface area for attack between Core and third-party codebases. WordPress also has nearly two decades of security hardening and the open-source ecosystem is heavily-scrutinized for threat vectors.

Using Proper Authentication & Permissions Checks.

Only execute what’s necessary

For both performance and security considerations, Admin-only code shouldn’t get instantiated unless is_user_logged_in() && is_admin(). Likewise, frontend code that isn’t needed in the Admin should confirm ! is_admin().

Check is_user_logged_in() AND `current_user_can()

It is never enough to check is_admin() in conditional logic. It should always be paired with is_user_logged_in() or better yet current_user_can().

One common issue is giving subscriber or lower-level users access to privileged data that should be isolated to editors or administrators or specific users using get_current_user_id(). Some WordPress products user the subscriber tier of users in unexpected ways, so checking WordPress’ system of capability strings instead of user role levels is considered best practice.

Using Nonces

WordPress uses a time-based nonce system to validate requests and prevent Cross-Site Request Forgery. These are not nonces in the strictest software concept of true nonces, as they’re not single use but expire over time – typically 12 hours.

We expect all WP-API endpoints or Admin Ajax callbacks to have nonce verification, with very few and highly-scrutinized exceptions.

Late-Escaping Markup

Cross-site scripting (XSS) mitigation is essential in WordPress codebases.

In PHP, we use WordPress Core functions like esc_attr(), esc_html(), esc_url() and wp_kses() to prepare strings for safe output to the DOM.

Escaping should always be done at the last-possible opportunity, even if that means repetitive wrapping of variables and data.

In JavaScript, we typically frown on inserting HTML directly into the DOM, either from an API response or concatonating variables into strings (i.e. '<a href="' + url '">'</a>). That means using .innerHTML and dangerouslySetInnerHTML() in React are discouraged. The one notable exception is rendered responses from data in the WordPress REST API on trusted sites. In lieu of these methods, creation of DOM nodes using .attr() and .text() is preferred or the use of React Components.

Validating Input & Sanitizing Database Writes

Note on JSON in metadata fields: Due to how update_metadata() works, JSON strings cannot be directly saved into meta fields without first passing through wp_slash() – without prior slashing invalid JSON will be returned.

We generally avoid new additions to admin-ajax.php

Before the WordPress REST API, the Admin Ajax file could be used for authenticated or unauthenticated requests.

As a general rule with few exceptions, we do not add new Admin Ajax callbacks when it is possible to register WP-API endpoints.

We also avoid the no_priv Admin Ajax hooks unless essential.

Storing/File-Logging HTTP Responses & Headers

It can be tempting to store an HTTP response in-full for caching or debug. Always assure you aren’t accidentally caching API tokens or passwords in cleartext that would normally be protected.

Always use $wpdb and $wpdb->prepare() for direct database queries

In general, we lean on WordPress’ internal query APIs like WP_Query, WP_Tax_Query and WP_User_Query for the caching and security built-in even if a direct query could eek-out a moderate performance gain.

When it becomes radically more performant or necessary to write direct SQL queries, never access the MySQL database directly using the credentials in the wp-config.php.

Instead we use the $wbdb global and use $wpdb->prepare() to sanitize queries or methods like esc_sql() and esc_like() – $wpdb->prepare() is considered best practice when applicable.

Beware caching $wpdb, which contains database credentials.

It can be tempting to cache a WP_Query or $wpdb

Beware of sensitive data in browser’s localstorage and sessionstorage.

As a general rule, browser localstorage should only be used for progressive enhancement, but when leveraged avoid storing sensitive tokens, content or user data in browser data stores.

wp_safe_redirect and allowed_redirect_hosts filter with exit()

When making a redirect in PHP, we expect the host to be in the allowed_redirect_hosts filter and proper use of wp_safe_redirect() paired with exit.

Often, the best way to do this is:

if ( wp_safe_redirect( $resource ) ) {
    exit;
} else {
    new \WP_Error();
}