Ask most WordPress owners how they would find out that one of their plugins has a published vulnerability, and the honest answer is that they would read about it somewhere, eventually. The update screen does not tell you. It tells you a newer version exists, which is a different thing: most updates fix nothing security-related, and the one that matters looks exactly like the twenty that do not.

There is a public database of WordPress vulnerabilities, it has an API, and the free tier is enough for a single site. This article shows how to query it properly, including the two places where a naive check quietly gives you the wrong answer.

Why an outside-in scan is not enough

The usual approach is to point a scanner at your URL and let it work out what you are running. It reads the page source, looks for tell-tale asset paths, and guesses versions from whatever the site leaks.

A hardened WordPress leaks nothing. It removes the generator tag, strips version query strings from assets, and blocks the user enumeration endpoints. That is good hygiene, and it means the outside-in scan now misses things silently. It does not tell you it could not see your plugin list. It returns a short, clean-looking report, and a clean-looking report about a site you have deliberately made opaque is the most misleading output a security tool can produce.

If you own the site, you do not have to guess. WordPress will tell you exactly what is installed and at which version, over its own REST API.

Step 1: Ask your site what it is running

Two endpoints give you the whole inventory:

GET /wp-json/wp/v2/plugins
GET /wp-json/wp/v2/themes

Each entry carries a name, a version, a text domain and a status of active or inactive. That is everything a vulnerability lookup needs.

Authenticate with an Application Password rather than your login password. In WordPress, go to Users, open your profile, and create one under Application Passwords. It is a separate credential you can revoke on its own, without changing the password you actually log in with.

curl -s -u 'yourname:xxxx xxxx xxxx xxxx xxxx xxxx' \
  https://example.com/wp-json/wp/v2/plugins

The uncomfortable part: this needs an administrator

Reading the plugin list requires the activate_plugins capability. Not read, not edit_posts. WordPress treats knowing what is installed as an administrative act, and there is no lesser role that can do it. An Editor account gets a 403.

So the credential your scanner holds is administrator-level, and it deserves to be treated that way. Create it for this purpose alone, keep it somewhere encrypted, revoke it the moment you stop using it, and never reuse it for anything else. Anyone who tells you a vulnerability scan of this kind can run on a read-only credential has not tried it.

Step 2: Get a WPScan API token

Register at wpscan.com/api. The free tier gives you 25 lookups a day, and one lookup means one plugin, one theme, or WordPress core.

Calls are authenticated with a header, and the word Token and the space after it are part of the value:

curl -s -H 'Authorization: Token token=YOUR_TOKEN_HERE' \
  https://wpscan.com/api/v3/plugins/contact-form-7

Get that header slightly wrong and you receive a 401, which is easy to mistake for a token problem when it is a formatting problem.

Check your remaining quota before you spend it, because the endpoint that reports it is free:

curl -s -H 'Authorization: Token token=YOUR_TOKEN_HERE' \
  https://wpscan.com/api/v3/status

Step 3: Match against the version you actually run

This is where most home-grown checks go wrong, and the failure is invisible because the output still looks plausible.

A lookup returns every vulnerability ever recorded for that plugin. Each entry carries a fixed_in version. A vulnerability affects you only when your installed version is older than fixed_in. A popular form plugin has eight entries going back to 2014, and on a current release none of them apply to you.

Report all eight and you have built the tool everybody ignores. The point of the exercise is the filter, not the list.

Compare numerically, never as strings

Version strings sort in an order that is not version order. As text, 1.10 is smaller than 1.9, because the character 1 comes before 9. As a version, 1.10 is newer.

Get that backwards and you produce one of two failures. Either you miss a vulnerability that does affect you, or you raise the same false alarm every night until the reports stop being read. Split on the dots, compare each segment as a number, and treat a missing segment as zero.

Step 4: Keep three outcomes apart

A lookup can end three different ways, and collapsing them into a pass or fail is the single most damaging thing you can do with this data.

  • Checked and clean. The plugin is in the database, and nothing recorded affects your version. This is a real result.
  • Not in the database. The API returns a 404. Most commercial and custom plugins are simply absent. This is not a clean result and must never be printed as one.
  • Lookup failed. An expired token, an exhausted quota, a network error. You know nothing about that component tonight.
  • The second and third are what turn a security report into false reassurance. A report that says “no issues found” after failing to check anything is worse than no report, because it actively tells you to stop worrying.

    Make the failure loud. If any lookup failed, say so in the subject line, and send the email whether or not you asked to be alerted only about findings. A monitor that goes quiet when it breaks looks identical to a monitor with nothing to report.

    Step 5: Live inside the quota instead of hitting it

    Twenty-five lookups a day sounds generous until you count your site. A modest install runs 25 to 30 plugins before themes and core, and a busy one comfortably exceeds 40. You cannot check everything every night on the free tier.

    The wrong response is to check the first 25 alphabetically, which means the plugins at the end of the alphabet are never checked at all. The right one is to remember when each component was last looked at and take the least recently checked first. Coverage then rotates, every component gets checked every couple of nights, and the report tells you how many were deferred so the gap is visible rather than assumed away.

    Storing that history is a four-column table: component, kind, when it was last checked, and what was found.

    A note on severity

    The free tier returns no CVSS score. The field is present and null. What you do get is a vulnerability type, such as a cross-site scripting or an authenticated privilege escalation, and you can band those into rough severities yourself.

    Two rules if you do. Say in the report that the banding is yours, so nobody mistakes it for an industry score. And when you meet a type you have not classified, label it unknown rather than defaulting it to low. A default of low is an assertion that something is minor when you have not looked, and it is exactly the class of vulnerability nobody has bothered to categorise that is worth reading about.

    What this approach still cannot tell you

    Worth being explicit, because a version check is often mistaken for a security audit.

    • It does not find malware already on your server. It never reads a file.
    • It says nothing about weak passwords, file permissions or misconfiguration.
    • It only knows what has been published. A vulnerability nobody has reported is invisible to it.
    • It checks WordPress core only if you tell it your core version, because a hardened site does not publish it.
    • What it does do is answer one question accurately and every night: is anything I run known to be vulnerable at the version I have installed. That question is worth automating, and on a fully patched site the correct answer is nothing at all.

      Get the template

      Everything above is what WordPress Security Scan does on a schedule. It is a free n8n template: it reads your plugin and theme list over the REST API, checks the least recently checked components first inside whatever quota you have left, compares every recorded vulnerability against your installed version numerically, and emails you the result. Not in the database is reported separately from clean, a failed lookup makes the subject line say the scan was incomplete, and severity bands are labelled as ours rather than dressed up as CVSS.

      The setup notes, including the exact columns for the history table and the header format for the API token, are built into the workflow canvas, so you do not need to keep this page open while you configure it.

      If you are putting the rest of your self-hosted stack on a schedule too, the same approach covers database backups without SSH and nightly GitHub repository backups.

DataDrifter is a marketplace of automation tools, scripts, and templates - built for SMEs and technical teams who want to move faster. A product of Elyxia Global Limited.

Data Drifter © 2025 - 2026, All rights reserved.