Skip to content

Three checks to see whether a site has been compromised: checksums, admin accounts, and PHP in uploads

Three checks to see whether a site has been compromised: checksums, admin accounts, and PHP in uploads

Updates keep things out. But how do you check whether something is already in? This article walks through three checks you can run with WP-CLI: verifying core and plugin checksums, taking stock of administrator accounts, and looking for PHP files under uploads. We cover how to read each result and where it stops being useful. These checks are defensive routine work, and a clean result is not a guarantee of safety.

Why check for “already inside”

Applying updates and removing unused plugins only makes the site harder to get into from now on. A survey we looked at in the previous article on exploit timing suggests the window between disclosure and exploitation can be short, so we cannot rule out that something arrived before the update.

Maintenance therefore needs a second viewpoint: is the current state the same as the state it should be in? All three checks below are read-only. They do not change the site.

Check What it confirms Main command
① File match Core and plugin files equal the published originals wp core verify-checksums / wp plugin verify-checksums
② Accounts No administrator you do not recognize wp user list --role=administrator
③ Location No PHP where code does not normally live find over uploads/

① Compare files with the published checksums

As described in our hash and checksum basics article, changing even one character of a file changes its hash. WP-CLI applies this idea to WordPress.

wp core verify-checksums

The command downloads the MD5 checksum list for your installed version from WordPress.org and compares it with the files on disk. According to its documentation, it avoids loading WordPress while verifying, so tampered code is not executed during the check.

A clean run prints:

Success: WordPress installation verifies against checksums.

When something differs, you get a per-file warning, such as a checksum mismatch or a file that should not exist.

Points to keep in mind:

  • Locale and version: if verification misbehaves, pass --locale and --version to match the values on the Updates screen, since localized packages differ
  • Root directory: by default the check covers core files; add --include-root to also warn about unfamiliar items in the site root
  • Your own edits: if you edited a core file, a mismatch is expected. What matters is whether you can explain every mismatch

Plugins work the same way

wp plugin verify-checksums --all

This works for plugins distributed through the WordPress.org directory. The important part is not overlooking what could not be checked. Paid plugins and plugins distributed elsewhere have no published checksums, and the tool reports that it cannot verify them. That is not itself a problem, but it is a gap in your coverage and should be covered by other means, such as the vendor’s change history or file modification times.

What checksum verification cannot do

  • It cannot check themes or plugins that have no published checksums
  • It ignores wp-config.php, uploads/, and files you created, because no reference exists
  • It cannot see database content, such as options or posts that contain unwanted code
  • It cannot detect a case where the distribution source itself was tampered with (see our supply chain article)

“Matches” means “same as the published original”, not “safe”.

② Take stock of administrator accounts

A trace that often remains after an intrusion is an administrator account you do not recognize. Changing a password does not remove it, and an unfamiliar admin remains a way in.

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered

Filter by role and include the registration time. Then ask:

  • Can you explain everyone? Who each admin is and why they need that role
  • Do the registration dates make sense? Any account created when nobody was working on the site
  • Are the email addresses expected? Former staff, past agencies, or unused mailboxes
  • Are there too many admins? Often an Editor role is enough

If you find an unfamiliar account, do not delete it on the spot. Deleting removes evidence. Record the situation first.

Fewer administrators is better in general. A periodic pass that downgrades or removes unused accounts shrinks the number of entrances.

③ Check for PHP files under uploads

wp-content/uploads/ holds images and documents added through the media library. Normally it contains no PHP files, because a place for media has no reason to hold executable code.

The check is read-only:

find wp-content/uploads -type f -name '*.php'

No output means this check found nothing. If something is listed:

  • No idea where it came from: do not open it in a way that runs it. Record the path and modification time
  • A plugin placed it on purpose: some plugins write generated files there. Confirm which plugin and whether you can explain it

A nearly empty index.php is a common convention that prevents directory listing and is easy to recognize by its content.

A step further is to configure the server so PHP does not run in uploads at all. That reduces entrances rather than detecting intrusions, and the method differs by server, so we will cover it separately.

Make it a routine

These checks are more useful when run the same way on a schedule, looking at the difference from last time.

  • Record the date and the outcome, including warnings
  • Until you can explain a warning, do not treat it as “no problem”
  • Run them after updates too, to see whether anything unfamiliar arrived

If you manage several sites, switching between them with aliases keeps the procedure consistent. Judge the results one site at a time with your own eyes.

If something looks wrong

  1. Record the command, the output, and the time. Do not delete or overwrite anything yet
  2. Confirm it is really abnormal: is it your own change or a plugin’s legitimate behavior?
  3. If it still looks abnormal, move to a response that includes recovery. Whether you have a backup you can restore to a clean state decides much of what follows

We will cover the response steps in a separate article. If you are unsure, consult a specialist.

Summary

  • Updates protect the future. Checking for what is already inside is the other half of maintenance
  • wp core verify-checksums and wp plugin verify-checksums compare with published hashes. Treat what could not be verified as part of the result
  • Review administrators with wp user list --role=administrator until you can explain each one
  • PHP under uploads/ is normally unexpected. If you find it, record it instead of running it
  • A clean result is not proof of safety. A habit of comparing with last time is what makes the checks useful

One thing to do today: run wp user list --role=administrator and confirm you can explain every name and why it exists.