Patchstack's 2026 report found that 91% of the WordPress vulnerabilities disclosed in 2025 were in plugins. Only six were in core.
So the plugin list is where the checking time goes. Here's a version of that check you can run from a shell, with WP-CLI and curl.
1. Get the real list, inactive plugins included
wp plugin list --fields=name,status,version,update --format=table
Keep the inactive rows. A deactivated plugin is still on disk, and its PHP files can still be requested by URL.
You can see that from outside. Most plugins ship a readme.txt with a Stable tag line:
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/wp-content/plugins/contact-form-7/readme.txt"
A 200 means anyone can read which version you run, active or not. That's the same file a scanner reads before it picks an exploit.
If a plugin isn't used, delete it:
wp plugin list --status=inactive --field=name
wp plugin delete <slug>
2. Check the plugin is still maintained
The wordpress.org plugin API returns the last update, the "Tested up to" version and the install count:
curl -sg "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]=contact-form-7&request[fields][sections]=0" \
| jq '{version, last_updated, tested, active_installs}'
A last_updated more than a year old is a warning sign. When a flaw turns up in an abandoned plugin, nobody ships the fix.
3. Look up the vulnerability records
Four public sources hold WordPress records:
| Source | Access |
|---|---|
| Patchstack database (patchstack.com/database) | Web search |
| WPScan (wpscan.com) | Web search, plus an API with a free token |
| Wordfence Intelligence | Web search |
| NVD (nvd.nist.gov) | CVE records with CVSS scores |
The WPScan API answers per plugin slug:
curl -s -H "Authorization: Token token=$WPSCAN_API_TOKEN" \
"https://wpscan.com/api/v3/plugins/contact-form-7" \
| jq '.["contact-form-7"].vulnerabilities[] | {title, fixed_in}'
Check a second source by hand. The databases don't always publish the same flaw on the same day.
4. Compare your version with fixed_in
The title of a record is the least useful part. The fixed_in field tells you whether you're still exposed.
# example values
installed="2.4.1"
fixed="2.4.3"
if [ "$(printf '%s\n' "$installed" "$fixed" | sort -V | head -n1)" = "$fixed" ]; then
echo "patched"
else
echo "VULNERABLE: update to $fixed or later"
fi
sort -V handles version strings like 5.10 vs 5.9 correctly. A plain string compare doesn't.
Then read two more fields on the record:
| Field | What it changes |
|---|---|
| Authentication | A flaw that works without a login goes first |
| Type | SQL injection and remote code execution are urgent |
An empty fixed_in means no fix exists. Patchstack's 2026 report says 46% of 2025 vulnerabilities had no patch at disclosure, so expect to see that often.
5. When there's no fix yet
Remove the plugin if the site can live without it.
If it can't, block the route the record names at the server, and cut the version leak from step 1. In nginx:
location ~* ^/wp-content/plugins/[^/]+/readme\.txt$ {
return 404;
}
On Apache, a .htaccess in wp-content/plugins/:
<FilesMatch "^readme\.txt$">
Require all denied
</FilesMatch>
That doesn't patch anything. It removes the version number a scanner uses to match your plugin to a record.
Changing the default plugin path goes one step further, because a scan of /wp-content/plugins/ then gets a 404 for every folder. WP Ghost can do that from the WordPress admin, next to the server rules above.
6. Repeat it
Put steps 1, 3 and 4 in a monthly cron or CI job, and run them by hand whenever a new plugin goes in.
The full checklist, with the wordpress.org page checks and the screenshots, is on the WP Ghost blog.
What do you use to watch plugin records across many sites, the WPScan API or something you built yourself?
Top comments (0)