Skip to Content
WordPress Plugins

WordPress Plugin Security Checklist: 2026 Practitioner Guide

WordPress Plugin Security Checklist: 2026 Practitioner Guide

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 forms
  • wp_create_nonce('my_action') for AJAX/REST clients
  • check_admin_referer('my_action', '_my_nonce') in admin form handlers
  • check_ajax_referer('my_action', '_my_nonce') in AJAX handlers
  • wp_verify_nonce($nonce, 'my_action') for manual verification
  • For REST: cookie auth requires X-WP-Nonce header (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 admins
  • current_user_can('edit_posts') — editors and above
  • current_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, trims
  • sanitize_textarea_field($input) — preserves line breaks
  • sanitize_email($input) — validates email format
  • sanitize_key($input) — alphanumeric + underscores + dashes only
  • absint($input) — non-negative integer
  • wp_kses_post($input) — HTML allowed (post-tier filter)
  • wp_kses($input, $allowed_html) — custom HTML allowlist
  • esc_url_raw($input) — URL for storage (different from esc_url for 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 content
  • esc_attr($value) — for HTML attributes
  • esc_url($value) — for URLs in attributes (href, src)
  • esc_js($value) — for JavaScript string literals
  • wp_json_encode($data) — for embedding in JS
  • esc_textarea($value) — for textarea content
  • wp_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)
  • %s for strings, %d for integers, %f for floats
  • %i (WordPress 6.2+) for identifier escaping (table/column names)
  • $wpdb->insert, $wpdb->update, $wpdb->delete auto-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_true in 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_mimes filter
  • Never store uploaded files in wp-content/plugins/ — use wp-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

Leave a Reply