Home › Guides › How to check if your WordPress site is hacked: 8 checks, including the ones most lists skip

How to check if your WordPress site is hacked: 8 checks, including the ones most lists skip

Short answer

Symptoms tell you something is wrong. These eight checks tell you where: core checksums, PHP in uploads, must-use plugins, administrators the Users screen hides, changed files, scripts in the database, the page a stranger sees and scheduled tasks. Each one has the exact command and the output that means trouble.

To check if your WordPress site is hacked, compare the core files with the official checksums, look for PHP files in the uploads folder and read the must-use plugins folder. Then count administrators straight from the database, list files changed since the site was clean, search posts and options for scripts, fetch a page as a logged-out visitor and read the scheduled tasks. Any one of these can turn up a hack the dashboard never shows you.

What are the first signs a WordPress site is hacked?

Most lists stop here, so this part is short. A browser warning on your domain, a sudden fall in search traffic, visitors who land on a spam or casino page, a fake “Verify you are human” box, or an email from your host about outgoing spam. Each of those means something has changed. None of them tells you what or where.

The eight checks below do. We ran every one of them on 20 October 2026 in a throwaway WordPress 7.1.2 site with WP-CLI 2.12.0. First we planted the kinds of change real attacks make: an edited core file, a stray PHP file, a hidden administrator, a script in a post and a widget, and a scheduled task. The output quoted on this page is what that site printed.

Most checks use WP-CLI over SSH. If your host gives you only a file manager and phpMyAdmin, each section says what to look at there instead. If you’d rather run the file and database checks in one go from the dashboard, Security Guard does five of the eight in a single read-only scan, and the section near the end says exactly which.

Why does the hack only show to logged-out visitors?

Because the people who planted it want it to last. Injected code can check whether the visitor is logged in, or has visited before, and stay quiet for anyone who might be the owner. You browse your own site as an administrator all day and see nothing wrong, while a first-time visitor from a search result gets the redirect or the fake CAPTCHA.

That is why several checks below read the files and the database directly instead of trusting what the dashboard shows. The dashboard runs the same plugins the attacker may have changed.

Have my WordPress core files been changed?

Run this from the folder that holds wp-config.php:

wp core verify-checksums

WP-CLI downloads the official checksums for your WordPress version and compares every core file against them. On our clean site it printed Success, with one warning: “File should not exist: wp-config-docker.php”. That file came with the Docker image, so it was expected.

After we edited wp-includes/version.php and added a new file next to it, the same command printed:

Warning: File doesn't verify against checksum: wp-includes/version.php
Warning: File should not exist: wp-includes/wp-cache-lib.php
Error: WordPress installation doesn't verify against checksums.

Trouble looks like that. A “doesn’t verify” line means a core file was edited. A “should not exist” line inside wp-admin or wp-includes means someone added a file where WordPress never puts one. Open each file it names before you change anything, and note the path.

The command also exits with status 1 instead of 0, so you can run it from a script or a cron job and act on the result.

Is there PHP in my uploads folder?

The uploads folder holds images and documents. Nothing in it needs to run as PHP, which is exactly why attackers like it. Start with a search by file name:

find wp-content/uploads -iname '*.php*'

In our test this caught thumb.jpg.php, a file named to look like an image. It did not catch logo.png, a file we had filled with PHP code but left with an image name. Searching the contents caught both:

grep -rl '<?php' wp-content/uploads

Any hit deserves a look. Some plugins put a small index.php in their own uploads folder to stop the folder being listed, and Security Guard does this for its private folder too. That kind of file is usually a line or two long. Anything longer, anything with a random name, or anything in a year and month folder such as uploads/2026/10/ is the kind you are looking for.

In a file manager, sort the uploads folder by type and search for .php. It won’t read inside files, so the content search above still matters.

What is in my must-use plugins folder?

Must-use plugins live in wp-content/mu-plugins. WordPress loads them on every request, they can’t be switched off from the Plugins screen, and they appear only on a separate Must-Use tab that many owners never open. List them:

wp plugin list --status=must-use

Our planted file, wp-cache-helper.php, was in the list. It followed a pattern Nintechnet described: on every page load it recreated an administrator called wpsupport, and it hid that user from the Users screen. We had set an old date on the file, so a plain directory listing showed it as last changed on 11 March 2024.

Plenty of hosts add their own must-use plugins for caching or staging, so a file here is not proof of a hack. For each one, ask who put it there. If neither you, your host nor a plugin you installed can explain it, treat it as hostile.

One trap: --skip-plugins does not skip must-use plugins. They still load, and they still run their filters on any WP-CLI command you type.

Are there administrators the Users screen does not show?

This is the check most guides miss. WordPress builds the Users screen with a user query, and any plugin can change that query through the pre_user_query filter. Nintechnet’s Jérôme Bruandet showed hackers doing exactly this in “How hackers create hidden admin users on your WordPress blog” (7 October 2020, updated 3 September 2021). Sucuri reported the same trick on 23 September 2025 in “Hidden WordPress Backdoors Creating Admin Accounts”: backdoors that “filtered user queries to hide the newly created admin account”.

So the obvious command can lie. On our test site, with the planted must-use plugin in place, this showed only the owner:

wp user list --role=administrator

It showed only the owner with --skip-plugins --skip-themes as well, because the filter lived in a must-use plugin. Asking the database directly does not go through that filter. The first line reads your table prefix, so the query works whatever the prefix is.

P=$(wp db prefix)
wp db query "SELECT u.ID, u.user_login, u.user_registered FROM ${P}users u JOIN ${P}usermeta m ON m.user_id = u.ID WHERE m.meta_key = '${P}capabilities' AND m.meta_value LIKE '%administrator%'"

That returned two rows: the owner and wpsupport. Our test site used the prefix kx_, and the common version of this query with wp_users typed in failed with “Table ‘wp.wp_users’ doesn’t exist”. Read the prefix, don’t assume it.

wp db query hands the SQL to the mysql or mariadb command line client, so that client has to be installed on the server. The official WordPress Docker image doesn’t include one. If yours is missing, run the same SELECT in phpMyAdmin or your host’s database tool.

There is also a quick clue on the Users screen itself. Our site showed “Administrator (2)” in the role links at the top, while the table underneath said “1 item”. The role counts come from a different function that the filter did not touch. If those two numbers disagree, someone is hiding an account.

Which files changed since the site was last clean?

The usual advice is to list recently modified PHP files:

find . -type f -name '*.php' -mtime -30

It missed our planted must-use plugin. The file was a day old, but we had set its modification date back to 11 March 2024 with touch -d, which anyone with write access can do. touch -r does the same by copying another file’s date. Attackers call this timestomping, and it beats every check based on the modified date.

The change time is harder to fake, because the system sets it whenever a file’s details change, including when someone backdates it. This caught the file:

find . -type f -name '*.php' -ctime -1

The catch is noise. Restoring a backup, updating a plugin or moving the site resets the change time on many files at once, so after any of those, the list is long and mostly innocent.

The reliable way is a baseline. Record a fingerprint of every file while you know the site is clean, then compare against it later. A changed fingerprint can’t be hidden by a date trick. Security Guard keeps a baseline like this, which the next section covers.

Is there a script in my posts or database options that I did not add?

Many infections never touch a file. They add a script tag to a post, a widget or a theme option, and WordPress prints it on every page. Search the whole database:

wp db search "<script" --all-tables

On our test site this found the script we had put in post 5 and the one in the widget_custom_html option. You could query the posts and options tables yourself with LIKE '%<script%', but wp db search covers every table in one go and you don’t need to know the table names.

Expect some honest hits. Analytics snippets, cookie banners and video embeds are scripts too. For each match, read the src address or the code. A domain you don’t recognise, a long string of encoded characters, or anything that writes to the clipboard is a reason to look closer. Our guide to the fake Cloudflare CAPTCHA shows what the clipboard kind looks like.

What does a logged-out visitor actually get?

Fetch the home page the way a stranger’s browser would, with no login cookie, and list the external scripts:

curl -sL -A "Mozilla/5.0" https://example.com/ | grep -oiE "<script[^>]*src=[\"']?[^\"' >]+"

Replace example.com with your domain. The -L makes curl follow redirects, from http to https or from the bare domain to www. Without it, a redirecting address prints nothing and looks clean.

The pattern also matters. The version you often see only matches src in double quotes, and on our test site it missed the script we had planted in a widget, because that one was written as src=https://bad.invalid/x.js with no quotes at all. The pattern above matches double quotes, single quotes and none. Go through the list and account for every domain.

Neither version lists inline scripts, the ones written straight into the page with no src. The database search above is how you find those.

Without a terminal, open the site in a private window on a phone using mobile data, so you are not logged in and not on your office network. Then view the page source and search it for “script”.

This is the one check that sees the site from the outside. It catches code that hides from administrators, and it catches a script injected by a plugin’s settings that never shows up as a file change.

Are there scheduled tasks I did not create?

A backdoor that someone removes can come back through a scheduled task that reinstalls it. List what WordPress plans to run:

wp cron event list --fields=hook,next_run_relative,recurrence

Ours showed a hook called wp_update_core_cache_9f2a, running hourly, sitting among the normal core hooks such as wp_version_check. The name was made to look like core. Core hook names don’t end in random characters.

For any hook you can’t place, search the code for its name with grep -rn "hook_name" wp-content. Whatever registered it is the file you want.

Can a scanner do these checks for me, and what will it miss?

Partly. Security Guard runs five of these checks in one read-only scan from the dashboard. It compares core files with the checksums from api.wordpress.org, looks for PHP in uploads, lists every must-use plugin, and compares files with the baseline you saved when the site was clean. It also matches known malware signatures in code files, posts and options.

Security Guard last scan: 5 high findings, including a fake verify prompt in a theme file, PHP in uploads and an eth_call loader in a post
A Security Guard Last scan card with 5 High findings, from a fake verify prompt in a theme file to a new administrator.

The scan runs on your server, and it never deletes or edits anything. It shows you what it found and leaves the decision to you.

Security Guard baseline: a saved snapshot of files and administrators to compare later scans against
Saving a baseline while the site is clean

Here is what it does not do, so you know which checks to keep running by hand:

  • It doesn’t count administrators with SQL. It lists the administrators WordPress reports and flags new ones. An account hidden by a pre_user_query filter won’t be in that list, so run the database query above.
  • It doesn’t read scheduled tasks. Use wp cron event list.
  • It doesn’t fetch pages as a logged-out visitor. Use curl or a private window.
  • It isn’t a firewall and it doesn’t clean anything automatically.
  • It works from signatures and your baseline. A brand new piece of malware may not match a signature, which is what the baseline is there to catch.

The Security Guard guide walks through the scan and the baseline screen by screen. If you are choosing between tools, our comparison of WordPress malware scanners sets out what each one checks, where its scan runs and whether it cleans or blocks.

I found something: what now?

Slow down before you delete anything. A file you remove in a hurry is evidence you no longer have, and a backdoor you missed will put it back.

  1. Take a copy of the site as it is. Files and database, hacked state included. You may need it to work out how they got in.
  2. Change every password. WordPress administrators, hosting panel, SFTP and the database user. Then replace the keys and salts in wp-config.php, which logs everyone out.
  3. Remove the accounts you found. Delete the hidden administrator with the database query results in front of you, and remove the must-use plugin that kept recreating it first, or it will be back on the next page load.
  4. Replace core, plugins and themes with fresh copies. Use the official downloads, not your backup, and run wp core verify-checksums again afterwards.
  5. Clear the database entries. Remove the scripts from the posts and options that wp db search found, and delete the scheduled task you could not explain.
  6. Run all eight checks again. Then save a new baseline, so the next change stands out on the day it happens.
  7. Find the way in. An outdated plugin or a reused password is the usual answer. Fix it, or the clean site will not stay clean.

If you would rather not do the clean-up yourself, ask your host whether they offer malware removal, or hire a site clean-up service. PluginSprout doesn’t clean hacked sites.

If you have a question about Security Guard, such as what a finding means or how the baseline works, ask us about Security Guard.

Security Guard. Find ClickFix and EtherHiding scripts, web shells and new admins on WordPress, then strip known injected scripts from pages. Runs on your own site.

View the add-on