Security in WordPress is mostly not a plugin you install. It is a set of checks your code makes every time: who is calling, did they mean it, is the input clean, is the output escaped. This page collects what we check in our plugins and on our own site.

Where security lives

flowchart LR
    C["Code<br/>nonces, capabilities, escaping"] --> R["Repository<br/>no secrets in Git"]
    R --> D["Deploy<br/>checks before release"]
    D --> S["Site<br/>hardening, maintenance"]
    S --> AI["AI access<br/>abilities with permissions"]

12 mistakes that fail plugin review

WordPress.org reviewers read patterns. Most rejections repeat from plugin to plugin:

#MistakeWhat reviewers look for
1No nonce checkAdmin action without check_admin_referer() or check_ajax_referer()
2Nonce without capability checkwp_verify_nonce() present, current_user_can() missing
3SQL without $wpdb->prepare()Request data concatenated into a query
4Unescaped outputecho $value without esc_html(), esc_attr(), esc_url()
5Stored XSSShortcode attributes or meta saved without sanitizing
6Public REST routespermission_callback => '__return_true' on routes that change data
7No capability check in AJAX or RESTThe action runs for anyone
8Local file inclusionA path built from request data without validation
9No ABSPATH guardA plugin file reachable directly
10unserialize() on untrusted dataObject injection
11Debug outputStack traces or file paths shown to users
12Obfuscated codebase64_decode with eval

Guard clauses on busy hooks

Some checks are about stability as much as safety. In 4WP FAQ two hooks run constantly: wp_head on every page and save_post on every save, autosave and revision.

TEXT
public static function render_schema() {
	if ( ! is_singular() ) {
		return;
	}
	global $post;
	if ( ! $post instanceof \WP_Post ) {
		return;
	}
	// build JSON-LD
}

public static function handle_post_save( $post_id, $post, $update ) {
	if ( ! $post instanceof \WP_Post ) {
		return;
	}
	if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
		return;
	}
	self::schedule_scan();
}

The snippet is shortened from the plugin code. Without these checks an archive page gets a fatal error and every autosave triggers extra work.

Our site: from commit to recovery

  • Before a commit: a pre-commit hook blocks .env files, keys, database dumps and tokens in code. CI repeats the check before every deploy. See Git.
  • On non-production environments: a must-use plugin blocks outgoing mail, so staging never writes to real users.
  • Maintenance: our secure maintenance practice case backs up the database, updates core and plugins, deactivates tooling that is unsafe on production and sets DISALLOW_FILE_MODS.
  • After a compromise: a WP-CLI-only recovery: look first, regain access, keep evidence, remove rogue users, clean files, update and harden, verify. Practice case in preparation.

AI access

When AI agents work with a site through the Abilities API:

  • Every ability needs its own permission_callback; never __return_true for anything that changes data.
  • Application Passwords used by connectors require HTTPS outside local environments.

FAQ

Is a security plugin enough?
It helps with the site, not with your code. Nonces, capabilities and escaping must be in the plugin itself.

Nonce or capability check?
Both. A nonce confirms intent; a capability confirms permission.

Where do I escape: on input or output?
Sanitize on input, escape on output, every time.

Related