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:
| # | Mistake | What reviewers look for |
|---|---|---|
| 1 | No nonce check | Admin action without check_admin_referer() or check_ajax_referer() |
| 2 | Nonce without capability check | wp_verify_nonce() present, current_user_can() missing |
| 3 | SQL without $wpdb->prepare() | Request data concatenated into a query |
| 4 | Unescaped output | echo $value without esc_html(), esc_attr(), esc_url() |
| 5 | Stored XSS | Shortcode attributes or meta saved without sanitizing |
| 6 | Public REST routes | permission_callback => '__return_true' on routes that change data |
| 7 | No capability check in AJAX or REST | The action runs for anyone |
| 8 | Local file inclusion | A path built from request data without validation |
| 9 | No ABSPATH guard | A plugin file reachable directly |
| 10 | unserialize() on untrusted data | Object injection |
| 11 | Debug output | Stack traces or file paths shown to users |
| 12 | Obfuscated code | base64_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.
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
.envfiles, 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_truefor 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.