WordPress plugin security: the 12-point checklist every plugin should pass
A practical 12-point WordPress plugin security checklist: nonces, capability checks, sanitization, escaping, prepared queries, safe file access, REST permissions, and more.
A WordPress plugin sits inside the same PHP process as the rest of your site. If it has a security flaw, the entire site has a security flaw. Here is the 12-point security checklist that every plugin should pass before going on a production WordPress install. Print it. Tape it next to your monitor.
1. Nonces on every form and AJAX endpoint
Use wp_nonce_field() in forms and check_admin_referer() or check_ajax_referer() on the receiving end. Without nonces your plugin is vulnerable to CSRF attacks where a malicious site can trigger admin actions in your dashboard while you are logged in.
2. Capability checks on every privileged action
Use current_user_can() with the right capability before any action that modifies data. "Logged in" is not the same as "authorized." A subscriber should not be able to call an endpoint that creates posts.
3. Sanitize every input
Use sanitize_text_field(), sanitize_email(), absint(), sanitize_key(), or wp_kses() based on the expected type. Never trust $_POST, $_GET, or $_REQUEST directly.
4. Escape every output
Use esc_html(), esc_attr(), esc_url(), esc_js() at the point of output, not at the point of storage. The escape function depends on context: esc_attr for HTML attributes, esc_url for URLs in href/src, esc_html for visible content.
5. Prepared SQL statements only
Always use $wpdb->prepare() for any query with user input. Never concatenate variables into SQL strings, even if you "trust" the source. SQL injection is the most common cause of full WordPress site takeover.
6. Direct file access blocked
Every PHP file in the plugin should start with if (!defined('ABSPATH')) exit;. This prevents anyone from running the file directly via /wp-content/plugins/your-plugin/some-file.php which would skip WordPress's authentication entirely.
7. File uploads validated by content, not name
Use wp_check_filetype_and_ext() with proper MIME type verification. Checking only the extension lets attackers upload shell.php.jpg and execute it.
8. No user input in file paths
Never concatenate user input into file_get_contents(), require, or include. Path traversal (../../wp-config.php) is a real attack vector. If you must accept a filename from the user, verify it is in a whitelist.
9. Secure storage of secrets
API keys for third-party services should not be stored in get_option() as plain text. Use the WordPress constants in wp-config.php, or at minimum a wrapper that base64-encodes them with a site-specific salt. Better yet, use the WP secrets vault patterns introduced in WordPress 6.5.
10. Cron events unscheduled on deactivate
If your plugin schedules events with wp_schedule_event(), it must call wp_clear_scheduled_hook() in its deactivation hook. Otherwise the cron event keeps firing into a deactivated plugin, which can produce errors or — worse — keep doing whatever it was doing if the function still exists.
11. Uninstall.php cleans up everything
Custom tables, options, transients, capabilities, scheduled events. The plugin should leave the database in the same state as before installation when uninstalled. Plugins that leave junk behind are a maintenance liability and a security exposure (old options sometimes contain stale credentials).
12. REST API permission callbacks
Every register_rest_route() call must include a permission_callback. Returning '__return_true' here is equivalent to leaving the door open to the public — and that is exactly what most WordPress security advisories in 2025 were about.
The 13th, unwritten rule
Test on a real WordPress install with WP_DEBUG enabled before shipping. Half of plugin security issues are caught by simply running the plugin with debug logging on for an hour. The other half are caught by following this checklist.
How IC pluginswp validates these for you
Every plugin generated by IC pluginswp is checked against this 12-point list as part of its automatic validation step. The plugin is also activated in a real WordPress 6.8 sandbox before it is delivered to you, with WP_DEBUG_LOG on, so any fatal error is caught and the plugin is repaired before you receive it. None of this is a substitute for your own review on a high-traffic site, but it is the kind of baseline that you would otherwise have to enforce manually for every freelance contract you commission.