2026年11月12日
2026年11月12日
WordPress Security Scans on a Schedule: Wordfence, WP-CLI and Cron
This article is a localized edition of the Japanese original. Read the Japanese original
WordPress compromises and malware often go unnoticed until something visible breaks. If you scan regularly and get the results by email, you find out much sooner.
This article covers three approaches. You can combine them.
| Approach | Good for | What you need |
|---|---|---|
| Wordfence automatic scans | Setting everything up from the admin screen | A plugin |
| A WP-CLI script run by cron | Servers you can reach over SSH | WP-CLI, cron, a way to send mail |
| A custom check run by WP-Cron | Getting told about new administrators or pending updates | Being able to put a file in mu-plugins |
The WP-CLI commands and Wordfence behavior were checked against their official documentation (as of November 2026).
Check the current state by hand first
If you have WP-CLI, run these commands in your WordPress directory.
# Check whether core files match the official release
wp core verify-checksums
# Check whether files of plugins distributed on wordpress.org match the release
wp plugin verify-checksums --all
# Plugins with available updates
wp plugin list --update=available
# Updates for WordPress itself
wp core check-update
# List administrators (look for accounts you don't recognize)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp core verify-checksumscompares core files such aswp-adminandwp-includesagainst the checksums published by WordPress.org. It reports modified files and files that shouldn't be there. It doesn't check anything insidewp-content(themes, plugins, uploaded files).wp plugin verify-checksums --allonly compares plugins distributed through the wordpress.org Plugin Directory. Paid and custom plugins have no checksums to fetch, so they are skipped.wp plugin listonly shows whether updates exist; it does not check for vulnerabilities. To check for known vulnerabilities, use a vulnerability database such as WPScan (see "Checking WordPress for vulnerabilities with WPScan").
Approach 1: Wordfence automatic scans
You can install Wordfence from the plugin screen. With WP-CLI:
wp plugin install wordfence --activate
How often Wordfence scans automatically depends on whether you use the free or paid version.
- Free: a quick scan runs every day and a full scan every 72 hours, automatically. You can't choose when they run.
- Paid (Premium): "Scan Scheduling" lets you choose the days and times for scans.
Manual scans are available at any time, even on the free version. For the scan screen and how to read the results, see "How to use the Wordfence scan".
Get an email when a scan finds problems
In Wordfence's "All Options", "Email Alert Preferences" lets you turn on email alerts for scan findings and choose the minimum severity to report (Critical, High, Medium or Low). The destination address is set in the general settings of the same "All Options" page ("Where to email alerts"). You won't get repeated emails about the same issue.
Wordfence sends mail through WordPress's mail function (wp_mail()). If the emails don't arrive, set up sending through SMTP with a plugin such as WP Mail SMTP.
Approach 2: Run a WP-CLI script daily from cron
If you can SSH into the server, you can bundle the WP-CLI checks into a script, run it daily from cron, and get email only when something is found.
Create /usr/local/bin/wp-security-scan.sh. Adjust WP_PATH and ADMIN_EMAIL to your environment.
#!/bin/bash
# Periodic WordPress check. Sends mail only when something is found
set -u
WP_PATH="/var/www/html"
ADMIN_EMAIL="admin@example.com"
REPORT="$(mktemp)"
FOUND=0
{
echo "=== WordPress periodic check: $(date '+%Y-%m-%d %H:%M') ==="
echo "--- Core file integrity ---"
wp --path="$WP_PATH" core verify-checksums 2>&1 || FOUND=1
echo "--- Plugin integrity (wordpress.org plugins) ---"
wp --path="$WP_PATH" plugin verify-checksums --all 2>&1 || FOUND=1
echo "--- PHP files inside uploads ---"
PHP_FILES="$(find "$WP_PATH/wp-content/uploads" -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) 2>/dev/null)"
if [ -n "$PHP_FILES" ]; then
echo "$PHP_FILES"
FOUND=1
fi
echo "--- PHP files changed in the last 24 hours (wp-content) ---"
find "$WP_PATH/wp-content" -type f -name '*.php' -mtime -1 2>/dev/null | head -50
echo "--- Plugins with available updates ---"
wp --path="$WP_PATH" plugin list --update=available --fields=name,version,update_version 2>&1
} > "$REPORT"
if [ "$FOUND" -ne 0 ]; then
mail -s "[WARNING] WordPress periodic check $(hostname)" "$ADMIN_EMAIL" < "$REPORT"
fi
rm -f "$REPORT"
{ … } > "$REPORT"collects only this run's results into a temporary file. If you searched a cumulative log instead, any warning that ever appeared would trigger an email every time.2>&1includes error output in the results. If you write it in the order2>&1 >> file, errors don't go into the file.wp core verify-checksumsandwp plugin verify-checksumsexit with a non-zero code when files don't match.|| FOUND=1catches that.- Recently changed files also appear after plugin updates, so the list is included for reference only and does not trigger an email.
Make the script executable, then run it once by hand and check the results.
sudo chmod 755 /usr/local/bin/wp-security-scan.sh
sudo -u www-data /usr/local/bin/wp-security-scan.sh
On Ubuntu/Debian, the mail command needs a package such as mailutils and an MTA that can send mail (Postfix, for example). Confirm that a test email arrives first.
echo "test" | mail -s "mail test" admin@example.com
WP-CLI warns and stops when run as root, so set up the cron job as the owner of the WordPress files (usually www-data with Apache or Nginx on Ubuntu/Debian).
sudo crontab -u www-data -e
This example runs every day at 3:30.
30 3 * * * /usr/local/bin/wp-security-scan.sh
If wp isn't found in www-data's environment, write it in the script as a path starting with /, such as /usr/local/bin/wp.
Approach 3: Get notified of new administrators and updates via WP-Cron
This code runs inside WordPress once a day. It checks whether administrators have been added since the last check, whether updates are pending, and whether there are PHP files in uploads, and emails you if it finds anything. So that switching themes doesn't stop it, save it as wp-content/mu-plugins/wpdr-security-check.php (create the mu-plugins folder if it doesn't exist).
<?php
/**
* Plugin Name: Periodic security check (daily email)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Register the daily event (does nothing if already registered)
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'wpdr_daily_security_check' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wpdr_daily_security_check' );
}
} );
add_action( 'wpdr_daily_security_check', function () {
// Admin functions aren't loaded during WP-Cron, so load them
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once ABSPATH . 'wp-admin/includes/update.php';
$issues = array();
// 1. Administrators added since the last check
$admin_ids = get_users( array( 'role' => 'administrator', 'fields' => 'ID' ) );
$admin_ids = array_map( 'intval', $admin_ids );
$known = get_option( 'wpdr_known_admin_ids' );
if ( is_array( $known ) ) {
foreach ( array_diff( $admin_ids, $known ) as $new_id ) {
$user = get_userdata( $new_id );
$issues[] = 'New administrator: ' . ( $user ? $user->user_login : $new_id );
}
}
update_option( 'wpdr_known_admin_ids', $admin_ids, false );
// 2. Core and plugin updates (from the update data WordPress fetches periodically)
$core = get_core_updates();
if ( is_array( $core ) && isset( $core[0]->response ) && 'upgrade' === $core[0]->response ) {
$issues[] = 'WordPress update available: ' . $core[0]->version;
}
foreach ( get_plugin_updates() as $plugin ) {
$issues[] = 'Plugin update available: ' . $plugin->Name . ' ' . $plugin->update->new_version;
}
// 3. PHP files inside the uploads folder (normally there are none)
$uploads = wp_get_upload_dir();
if ( is_dir( $uploads['basedir'] ) ) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $uploads['basedir'], FilesystemIterator::SKIP_DOTS )
);
foreach ( $files as $file ) {
if ( preg_match( '/\.(php\d?|phtml|phar)$/i', $file->getFilename() ) ) {
$issues[] = 'PHP file in uploads: ' . $file->getPathname();
}
}
}
if ( $issues ) {
$body = "The periodic security check found the following:\n\n";
$body .= '- ' . implode( "\n- ", $issues ) . "\n\n";
$body .= 'Checked at: ' . wp_date( 'Y-m-d H:i' ) . "\n";
$body .= 'Admin: ' . admin_url() . "\n";
wp_mail( get_option( 'admin_email' ), '[' . get_bloginfo( 'name' ) . '] Periodic security check', $body );
}
} );
get_plugins(),get_core_updates()andget_plugin_updates()live in admin-only files that aren't loaded while WP-Cron runs. Without therequire_oncelines, the check stops with an error.- Rather than asking "are there too many administrators?", the code compares against the list saved at the last check and reports anyone new. The first run only saves the list.
- Mail goes to the "Administration Email Address" under "Settings" → "General" (設定 → 一般 in the Japanese UI).
Because this code runs inside WordPress, if WordPress's own files have been altered, the malicious code could stop it. To detect tampering, combine it with a check from the server side using WP-CLI, as in Approach 2.
Confirm that the event is scheduled
wp cron event list --fields=hook,next_run_relative,recurrence | grep wpdr_daily_security_check
# Run it once now to see whether the email arrives
wp cron event run wpdr_daily_security_check
WP-Cron runs when the site gets a visit, so on low-traffic sites it can run late. To run at a fixed time, trigger WP-Cron from the server's cron (see "How to fix WP-Cron not running").
To stop using the code, delete the file, then remove the leftover event and option.
wp cron event delete wpdr_daily_security_check
wp option delete wpdr_known_admin_ids
When a problem is found
wp core verify-checksumsreports mismatched core files: if you didn't modify them yourself,wp core download --version="$(wp core version)" --skip-content --forceoverwrites the core files with the same version (wp-contentandwp-config.phpare not changed). If the site was compromised, though, the files will be modified again unless you close the way in (an outdated plugin, a leaked password and so on).- There are PHP files in
uploads: a normal WordPress install doesn't put files like that there. Check their contents, and if you don't recognize them, suspect malware.
For how to investigate and clean up a suspected infection, see "Detecting and removing WordPress malware".
Cautions
- Scans use server CPU and disk. If you can choose the time (Wordfence Premium or cron), pick a low-traffic period.
- A clean scan doesn't mean the site is safe. Keep plugins and themes updated, use strong administrator passwords and two-factor authentication, and keep backups.
- If the emails don't arrive, the setup is useless. After configuring it, confirm that a test email arrives.
Summary
- Use
wp core verify-checksumsandwp plugin verify-checksums --allto find files that differ from the official release (files insidewp-contentand paid plugins are not covered) - Wordfence free runs a quick scan daily and a full scan every 72 hours. Choose the alert severity in "Email Alert Preferences"
- The WP-CLI script looks only at the current run's results and sends mail only when something is found. Set up the cron job as the owner of the WordPress files
- Put the WP-Cron check in mu-plugins and load the admin files with
require_once
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)