WordPress plugin security is what keeps your plugin off WPScan’s vulnerability database. Most plugin CVEs in 2024-2025 were variations of the same five mistakes — missing nonces, missing capability checks, unsanitized user input, unprepared SQL, missing REST permission callbacks. None are exotic. All are preventable.
This is the security checklist I run on every plugin before shipping in 2026. It covers the WordPress-specific patterns (nonces, capabilities), the language-agnostic basics (sanitize on input, escape on output), and the modern attack surfaces (REST API, AJAX, Gutenberg). No abstract security theater — concrete checks.
Quick verdict: nonces on every state-changing form/AJAX/REST request, capability checks on every privileged action, sanitize input + escape output religiously, prepared SQL statements always, and REST permission_callbacks that actually check permissions. Pass these and you avoid 90%+ of plugin CVEs.
Nonces — verify every state change
Nonces protect against CSRF (Cross-Site Request Forgery). Every form, AJAX call, and state-changing REST endpoint needs nonce verification.
wp_nonce_field('my_action', '_my_nonce')in formswp_create_nonce('my_action')for AJAX/REST clientscheck_admin_referer('my_action', '_my_nonce')in admin form handlerscheck_ajax_referer('my_action', '_my_nonce')in AJAX handlerswp_verify_nonce($nonce, 'my_action')for manual verification- For REST: cookie auth requires
X-WP-Nonceheader (handled by core)
Nonce gotcha: Nonces in WordPress are NOT cryptographically unique — they are time-bound to a 12-24h window. They protect against CSRF but not replay attacks within the window. For high-security operations, layer additional checks (capability + nonce + recent action timestamp).
Capability checks — verify every privileged action
Nonces prove the request came from the user. Capabilities prove the user is allowed to do this. You need both.
current_user_can('manage_options')— only adminscurrent_user_can('edit_posts')— editors and abovecurrent_user_can('edit_post', $post_id)— object-level capability check- Define custom capabilities for plugin features —
my_plugin_manage_orders - Never trust frontend hidden form fields to determine permissions — always verify server-side
Sanitization on input
Sanitize ALL data received from users (forms, query strings, headers, files) before storing or using it.
sanitize_text_field($input)— strips tags, removes line breaks, trimssanitize_textarea_field($input)— preserves line breakssanitize_email($input)— validates email formatsanitize_key($input)— alphanumeric + underscores + dashes onlyabsint($input)— non-negative integerwp_kses_post($input)— HTML allowed (post-tier filter)wp_kses($input, $allowed_html)— custom HTML allowlistesc_url_raw($input)— URL for storage (different fromesc_urlfor output)
Escaping on output
Escape ALL data before outputting to HTML, attributes, JavaScript, or URLs. Output context determines the right function.
esc_html($value)— for HTML body contentesc_attr($value)— for HTML attributesesc_url($value)— for URLs in attributes (href, src)esc_js($value)— for JavaScript string literalswp_json_encode($data)— for embedding in JSesc_textarea($value)— for textarea contentwp_kses_post($value)— for HTML where some markup is intentional
SQL — always prepare, never concatenate
SQL injection is solved if you always use $wpdb->prepare(). There is no excuse to ship raw concatenated SQL in 2026.
$wpdb->prepare("SELECT * FROM table WHERE id = %d", $id)%sfor strings,%dfor integers,%ffor floats%i(WordPress 6.2+) for identifier escaping (table/column names)$wpdb->insert,$wpdb->update,$wpdb->deleteauto-prepare- For LIKE clauses:
$wpdb->esc_like($search) . '%'
REST API permission callbacks
The most common REST API CVE in WordPress plugins is missing or incorrect permission_callback. Treat every REST endpoint as a security surface.
- Never use
permission_callback => __return_truein production - Always check the appropriate capability for the action
- For object-level permissions, verify the specific resource the user is operating on
- Public read endpoints should still rate-limit (custom middleware or Cloudflare)
- Document which endpoints are public vs authenticated in your README
File uploads
File upload handling is a frequent vulnerability source. Use WordPress core helpers and never trust client-provided file types.
- Use
wp_handle_upload()— it does MIME type sniffing and core validation - Restrict allowed MIME types via
upload_mimesfilter - Never store uploaded files in
wp-content/plugins/— usewp-content/uploads/ - Never execute uploaded files (no PHP in upload directory)
- For sensitive uploads, store outside web root and serve via authenticated download endpoint
Common vulnerabilities to specifically check
The CVE patterns I see most often in plugin security audits:
- Missing nonces on AJAX — admin AJAX handlers without
check_ajax_referer - Missing capability checks on AJAX — anyone who is logged in can hit privileged actions
- Reflected XSS in admin pages — query strings echoed without escaping
- SQL injection in custom search — search query concatenated into LIKE
- SSRF via REST — endpoint that fetches a URL the user provides without allowlisting
- Path traversal in file deletion — endpoint accepts a filename param without validating it stays in expected directory
Tooling — FAQs
What WordPress security tools should I run on my plugin?
PHPCS with WordPress-Extra and WordPress-VIP-Go rulesets — they catch most issues automatically. PHPStan or Psalm for static analysis. Plugin Check (the official wordpress.org tool) before submitting to the directory. WPScan database to monitor for similar plugins’ CVEs you might inherit. Manual code review for permission checks (tooling cannot reliably verify business logic).
Should I get a third-party security audit?
For commercial plugins with 10k+ active installs or plugins handling payments/PII: yes. Cost is $3k-$15k for a serious audit. For smaller plugins, internal review using the checklist + automated tooling covers 90% of issues. The audit ROI scales with installation count and data sensitivity.
How do I handle vulnerability disclosure?
Publish a security.txt or SECURITY.md with a contact email and PGP key. Respond to reports within 24-48h. Fix in private branch, ship the fix, then publish a CVE/advisory. Patchstack is increasingly used for the WordPress ecosystem and gives you free vulnerability tracking. Never threaten security researchers.
Operations — FAQs
How often should I update plugin dependencies?
Monthly for normal updates. Same-day for security advisories on packages you use. Use Dependabot or Renovate to automate PRs for dependency updates. Run your test suite on every dependency PR to catch breaking changes before merge. Lag in dependency updates compounds into expensive multi-version-jump migrations.
Do I need to worry about supply chain attacks via Composer?
Yes — typo-squatting, package takeover, malicious updates are real. Mitigations: pin versions in composer.lock, audit new dependencies before adding, use Mozart/PHP-Scoper to vendor-prefix in production builds (limits blast radius of a compromised package), monitor Composer security advisories.
How do I notify users when I patch a security issue?
For wordpress.org-hosted plugins: ship the update; the plugin directory + WordPress auto-update push to users. Add a changelog entry calling out the security fix. For premium/commercial plugins: same plus email notification to active license holders for critical issues. Always file a CVE for tracked vulnerabilities so the community ecosystem (Patchstack, WPScan) can warn affected users.
Need a security review on your plugin or a new plugin built secure from day one?
Plugin security advisories almost always trace to the same root causes: missing nonces, unescaped output, unvalidated input, broken capability checks. I build WordPress plugins with security baked in — nonce verification, input sanitization, output escaping, capability checks on every action — so your plugin never shows up on the WPScan advisory feed.
See my WordPress plugin development service
